Pascal Game Development
 

Pascal Gamedevelopment Library : TTCF3

PgdLib :: Categories :: PageIndex :: RecentChanges :: Login/Register

Tripping The Class Fantastic - Versioned Data Storage

A practical application of dynamic method calls


Hi and welcome to another of my articles. This time around I'm going to be covering a couple of topics. The first is pretty obvious from the title... versioned data storage, or to be specific, a simple method of allowing your software to provide a level of backwards compatibility with older versions of its data whilst simultaneously allowing it to update the store to reflect the format of the latest version *Deep breath*

To do this, without creating code that is awash with if..then..else and case statements, I'm going to use dynamic method calls. Not to be confused with the DYNAMIC keyword, these are calls to methods whose names we don't actually know at compile time. So you can experiment (or use the ideas) straight away, I'm also going to present a base class that handles the generic load/save routines for you... all you have to do is provide your implementation specific routines.

This article and the code should work 100% with Delphi (I can't comment on other flavours of Pascal... maybe others could post comments about the support for this technique within their favourite flavour).

So lets get started...

We, as games developers, are (from time to time) likely to require large local data stores for our various media elements. To handle our local storage requirements we have various options open to us. Streaming is one such option.

I'm sure a lot of people will be using (or have at least experimented with) Delphi's built in streaming mechanisms to store data... they are great, but they can have some severe limitations... largely related to the addition (or removal) of items of data. Many times in the past I've created datastores, found I needed to add extra fields to the objects in my code only to be faced with the prospect of recreating my data stores just to add the missing fields.

The answer... versioning... there are lots of possible options here, but I like simplicity where possible as its one less thing to go wrong, so my approach is to write a version header (a single integer) at the start of the objects data within the stream to indicate the version of the data it contains. Reading it back, I read the version indicator and then select the corresponding method for handling the stored data format. There is a little more to it (I don't use TPersistent and I certainly don't use Delphi's built in component streaming), but the crux of it is selecting the required methods when the data is being read and written.

I would imagine everyone is saying use 'if..then..else' or better 'case..'.

If you only have one or two versions these approaches are OK, but they can quickly become an ugly mess.

 if (version=1) then load_version001(src) else
  if (version=2) then load_version002(src) else
  if (version=3) then load_version003(src) else
  if (version=4) then load_version004(src) else
  if (version=5) then load_version005(src) else
  if (version=6) then load_version006(src) else
  if (version=7) then load_version007(src) else
  raise exception.create('Unsupported file version!');


And thats only 7 versions... during the PGD competition I went through 6 versions of map data and 10 versions of editor data store. And all without actually changing a single line of the code that was handling previous versions or the main load and save routines (thats not strictly true as removing an item of data does require slight modification of older load and save routines, but we'll come to that later).

Before I go any further I think I should just point out the limitation that applies to using this approach... whilst it sounds great... 'you can call a method without knowing its name at compile time' you do have to know a little bit about the method... namely the parameters it expects and any return types for functions.

Now we've got the general idea, lets get into some code. The first thing we need to address is the issue about knowing the parameters and returns types when we finally call our method.

To handle this, we declare prototypes, just like we do for event handling.

type
  TMyDynamicProcedure = procedure(param1:integer;param2:string) of object;
  TMyDynamicFunction = function(param1:string):boolean of object;


In this example, we have declared two prototypes. One procedure and one function. As far as I know, these declarations can use any of the standard rules that apply when defining procedures and functions (variable parameters, constant parameters etc.), but I haven't tried every possible permutation so you may find you need to tweak your code if a particular combination isn't acceptable to the compiler.

Once we have our prototypes, we are pretty much set to start implementing the actual calls. There is just one more rule that we need to be aware of... for this to work, the methods you plan on calling in this way need to be declared as PUBLISHED. This is to ensure that the required information about the methods is available at runtime.

So lets make a call. The only things we need are a couple of variables that will be used during the process and the prototypes of the methods you want to call.

The variables...

var
   methodPtr     : pointer;
   method        : TMethod;


The first (MethodPtr) is used to hold the result from the function that is used to locate the required method. The second (Method) is a base prototype equivalent to 'procedure of object'. It has two fields 'Data' and 'Code'. Code is used to provide the entry point for the required method (from MethodPtr) and Data provides a link to the object to which the method belongs. I've only ever been in a situation where Data is set to Self, so I can't comment any further on whether it works if you call methods in another object. However for the sake of clarity I have used a variable called 'targetObject' which we will for this article assume is set to 'Self'.

Anyhow, lets get the rest of the code covered. It is essentially quite simple.

The first thing we need to do is to try and locate the method we want to call. The name of this method is in the string variable 'methName'.

methodPtr:=targetObject.methodAddress(methName);


If methodPtr is nil, we haven't found the method... the course of action you take here will obviously depend on the application. If methodPtr is not nil, then we've found a method and we can get on and run it like this...

 method.data:=targetObject;
  method.code:=methodPtr;

  TMethodPrototype(method)(params);


And that as they say is that. Functions will obviously require a variable to receive the result and the parameter lists will need to match the prototype declarations.

So, lets put this to some real use... a basic object that can be used as the basis for a versioned data store.

This unit is derived from my own streamed object. It just doesn't include some of the more exotic functions that make it more functional. It doesn't do anything fancy, but it works.

unit streamedObject;

interface

uses
    classes, sysUtils;

type
    (*---Method Prototypes (Required for dynamic procedure calls)---------------------------------------------------------*)

    TStreamingReadPrototype = procedure(reader:TReader) of object;
    TStreamingWritePrototype = procedure(writer:TWriter) of object;

    (*---TORSStreamedObject---------------------------------------------------------*)

    TStreamedObject = class(TObject)
    private

    protected

      fObjectVersion     : integer;
      fVersionInFile     : integer;

      procedure raiseEx(msg:string);

    public
      constructor create;

      procedure load(reader:TReader); virtual;
      procedure save(writer:TWriter); virtual;

      procedure loadFromStream(src:TStream); virtual;
      procedure saveToStream(dst:TStream); virtual;

      property objectVersion:integer read fObjectVersion;

    published

      // Declare these in the derived class (they MUST be published)
      // procedure load_verx(reader:TReader); virtual;
      // procedure save_verx(writer:TWriter); virtual;
     
    end;

    (*---Exception class---------------------------------------------------------*)

    EStreamedObjectException = exception;

implementation

const
     _streamedObjectBufferSizeLoad = 2048;
     _streamedObjectBufferSizeSave = 2048;

(*---Exception raiser---------------------------------------------------------*)

procedure TStreamedObject.raiseEx(msg:string);
begin
     raise EStreamedObjectException.create(self.className+msg);
end;

(*---Constructor---------------------------------------------------------*)

constructor TStreamedObject.create;
begin
     inherited create;

     fObjectVersion:=1;
end;

(*---IO Method Selectors---------------------------------------------------------*)

procedure TStreamedObject.load(reader:TReader);
var
   methodPtr     : pointer;
   method        : TMethod;
begin
     try
        fVersionInFile:=reader.ReadInteger;
     except
       on e:exception do
          raiseEx('.load - Exception occured reading version, message was '+e.message);
     end;

     if (fVersionInFile<1) or (fVersionInFile>fObjectVersion) then
        raiseEx('.load - Expecting version 1..'+intToStr(fObjectVersion)+', found '+intToStr(fVersionInFile));

     methodPtr:=nil;

     while (fVersionInFile>0) and (methodPtr=nil) do
           begin
                methodPtr:=self.methodAddress('Load_Ver'+intToStr(fVersionInFile));
                if (methodPtr=nil) then
                   dec(fVersionInFile);
           end;

     if (methodPtr<>nil) then
        begin
             method.data:=self;
             method.code:=methodPtr;

             TStreamingReadPrototype(method)(reader);
        end
     else
         raiseEx('.load - Could not locate load method Load_Ver'+intToStr(fObjectVersion));
end;

procedure TStreamedObject.save(writer:TWriter);
var
   methodPtr     : pointer;
   method        : TMethod;
begin
     try
        writer.writeInteger(fObjectVersion);
     except
       on e:exception do
          raiseEx('.save - Exception occured writing version, message was '+e.message);
     end;

     fVersionInFile:=fObjectVersion;

     methodPtr:=nil;

     while (fVersionInFile>0) and (methodPtr=nil) do
           begin
                methodPtr:=self.methodAddress('Save_Ver'+intToStr(fVersionInFile));
                if (methodPtr=nil) then
                   dec(fVersionInFile);
           end;

     if (methodPtr<>nil) then
        begin
             method.data:=self;
             method.code:=methodPtr;

             TStreamingWritePrototype(method)(writer);
        end
     else
         raiseEx('.save - Could not locate save method Save_Ver'+intToStr(fObjectVersion));
end;

(*---Main IO Routines---------------------------------------------------------*)

procedure TStreamedObject.loadFromStream(src:TStream);
var
   reader        : TReader;
begin
     if (src.size>0) then
        begin
             reader:=TReader.create(src,_streamedObjectBufferSizeLoad);

             try
                self.load(reader);
             finally
               try
                  reader.free;
               except
               end;
             end;
        end
     else
         raiseEx('.loadFromStream - Stream is empty');
end;

procedure TStreamedObject.saveToStream(dst:TStream);
var
   writer        : TWriter;
begin
     writer:=TWriter.create(dst,_StreamedObjectBufferSizeSave);
     try
        self.save(writer);

        writer.flushBuffer;

     finally
       try
          writer.free;
       except
       end;
     end;
end;

end.


I'm not going to do a full breakdown of the code as for the most part it is pretty self explanatory and not too complex. The only thing I really want to discuss is the method search section of the load and save methods.

In both cases, the routines start at a high version (the current fObjectVersion in the case of save and the version read from the stream in the case of load) and work down to 1 looking for the methods load_verx and save_verx respectively.

This makes our data store capable of handling older formats and ensures that when we save the store (during development/editing) it is always stored using the latest data format.

And, in case you're new to streams, lets just mention TReader and TWriter.

TStream (and its descendants) doesn't actually have any methods for reading and writing say an Integer. You can use Read and Write, but then you have to concern yourself with addresses and variable sizes. The easier way (as I have done) is to use TReader and TWriter. These are helper objects that provide methods for direct storage and retrieval of data... TReader.readInteger and TWriter.writeInteger for example. This is not a perfect solution as by denying access to the source and destination streams within the load_verxx and save_verxx methods, life gets difficult if you want to store a stream within the stream, but the trade off is worthwhile because for the majority of the time your life is made considerably easier by using TReader and TWriter.

If you are wondering why you would want to save a stream within a stream... consider TCollection... you have a collection object within your data store object... the easiest way to load and save it is to use its own loadFromStream and saveToStream methods. One approach would be to use an intermediate stream (TStringStream) for example... save the object to the TStringStream and then save the TStringStream's 'DataString' property using TWriter.writeString. Its a bit clunky, but it is one approach to loading and saving streams within the load_verx and save_verx methods.

So thats the basics... now lets put it to use.

interface

uses streamedObject;

type
  TMyDataStore = class(TStreamedObject)
  protected
    fMyIntData : integer;
    fMyStringData : string;
  public
    property myIntData:integer read fMyIntData write fMyIntData;
    property myStringData:string read fMyStringData write fMyStringData;
  published
    procedure load_ver1(reader:TReader);
    procedure save_ver1(writer:TWriter);
  end;

implementation

procedure TMyDataStore.load_ver1(reader:TReader);
begin
  fMyIntData:=reader.readInteger;
  fMyStringData:=reader.readString;
end;

procedure TMyDataStore.save_ver1(writer:TWriter);
begin
  writer.writeInteger(fMyIntData);
  writer.writeString(fMyStringData);
end;


Here we have declared the object whose data we want to store. If you have a single version, its that easy. To use the object to store the data you create a stream and call the appropriate method (loadFromStream or saveToStream) like this.

var
  aStream : TFileStream;
begin
  // To Save...
  aStream:=TFileStream.create('C:\MyData.dat',fmCreate);
  myDataStore.saveToStream(aStream);
  aStream.free;

  // To Load...
  aStream:=TFileStream.create('C:\MyData.dat',fmOpenRead);
  myDataStore.loadFromStream(aStream);
  aStream.free;
end;


Just a quick note in case its not obvious, you can of course store multiple objects in a single stream... you just have to be sure you read and write them in the same order.

Well, thats a basic case... so lets look at version 2. We have added the field 'MyIntData2'.

interface

uses streamedObject;

type
  TMyDataStore = class(TStreamedObject)
  protected
    fMyIntData : integer;
    fMyIntData2 : integer;
    fMyStringData : string;
  public
    constructor create;

    property myIntData:integer read fMyIntData write fMyIntData;
    property myIntData2:integer read fMyIntData2 write fMyIntData2;
    property myStringData:string read fMyStringData write fMyStringData;
  published
    procedure load_ver1(reader:TReader);
    procedure save_ver1(writer:TWriter);

    procedure load_ver2(reader:TReader);
    procedure save_ver2(writer:TWriter);
  end;

implementation

constructor TMyDataStore.create;
begin
  inherited;

  fObjectVersion:=2;
end;

procedure TMyDataStore.load_ver1(reader:TReader);
begin
  fMyIntData:=reader.readInteger;
  fMyStringData:=reader.readString;
end;

procedure TMyDataStore.save_ver1(writer:TWriter);
begin
  writer.writeInteger(fMyIntData);
  writer.writeString(fMyStringData);
end;

procedure TMyDataStore.load_ver2(reader:TReader);
begin
  load_ver1(reader);

  fMyIntData2:=reader.readInteger;
end;

procedure TMyDataStore.save_ver2(writer:TWriter);
begin
  save_ver1(writer);

  writer.writeInteger(fMyIntData2);
end;


Notice that now we have 2 versions we have to override the default value of fObjectVersion in the constructor. This object will save the data using the latest version, so it automatically updates data stores when they are saved.

One thing you will notice is that when you load a version 1 stream, the version 2 fields are not initialised. This can be addressed by initialising them in the version 1 loader, or by modifying the load method to call an initialisation function that sets all fields to a known state.

Now lets look at the version 3 code. Version 3 removes an item of data. This is a little trickier since we have removed the field that holds the data once it is loaded. The item we will remove is fMyIntData. To achieve this, it is necessary to make a modification to the routines that load and save this item (in this case the load_ver1 and save_ver1 methods) and to declare a new set of load and save code for version 3 and beyond.

interface

uses streamedObject;

type
  TMyDataStore = class(TStreamedObject)
  protected
    fMyIntData2 : integer;
    fMyStringData : string;
  public
    constructor create;

    property myIntData2:integer read fMyIntData2 write fMyIntData2;
    property myStringData:string read fMyStringData write fMyStringData;
  published
    procedure load_ver1(reader:TReader);
    procedure save_ver1(writer:TWriter);

    procedure load_ver2(reader:TReader);
    procedure save_ver2(writer:TWriter);

    procedure load_ver3(reader:TReader);
    procedure save_ver3(writer:TWriter);
  end;

implementation

constructor TMyDataStore.create;
begin
  inherited;

  fObjectVersion:=3;
end;

procedure TMyDataStore.load_ver1(reader:TReader);
var
  empty : integer;
begin
  // Since we no longer use fMyIntData, read its value into an empty variable
  empty:=reader.readInteger;

  fMyStringData:=reader.readString;
end;

procedure TMyDataStore.save_ver1(writer:TWriter);
begin
  // Write an empty value (just in case we save to version 1)
  writer.writeInteger(0);

  writer.writeString(fMyStringData);
end;

procedure TMyDataStore.load_ver2(reader:TReader);
begin
  load_ver1(reader);

  fMyIntData2:=reader.readInteger;
end;

procedure TMyDataStore.save_ver2(writer:TWriter);
begin
  save_ver1(writer);

  writer.writeInteger(fMyIntData2);
end;

procedure TMyDataStore.load_ver3(reader:TReader);
begin
  fMyStringData:=reader.readString;
  fMyIntData2:=reader.readInteger;
end;

procedure TMyDataStore.save_ver3(writer:TWriter);
begin
  writer.writeString(fMyStringData);
  writer.writeInteger(fMyIntData2);
end;


As you can see, removing items is trickier than adding, but I've found that I generally remove a lot fewer items than I add. Of course you can minimise your work by thinking ahead and designing your code before you get into the nuts and bolts of creating data stores. But if you do dive straight in and you find yourself with a lot of redundant items, I would advise removing them together. The reason is that everytime you remove an item, you need to rewrite the majority of the load_verx/save_verx code as shown in the example above. You will notice that when we add items we call the earlier routines (load_ver2 calls load_ver1 etc). But when we remove items its necessary to break the chain at the version they were removed in (so load_ver3 doesn't call earlier versions), this obviously necessitates writing a whole bunch of code that loads/saves all the fields you haven't removed.

That concludes this little introduction to dynamic method calls and their practical application in constructing versioned data stores. I hope you can see that there are many possibile applications of this ability to call methods dynamically at runtime, but care should be taken when considering whether it is appropriate. I haven't conducted any timings but my gut feeling is that this is not the quickest way in the world. Depending on your application, a big ugly IF..THEN..ELSE or CASE... may be a better approach. But where speed isn't important and you're IF..THEN..ELSEs are getting out of hand, this may be applicable.

With regards to the data storage method presented here, there are other ways to achieve this kind of thing, but with this system you have full control over whats saved and how, you don't have to expose every field as published/public in order to store it and above all, its easy to understand... even for beginners.

As usual, if you have any questions or comments, then please feel free to email me on athena at outer hyphen reaches dot com or post a comment on the article. Thanks for reading. Until next time... take care and happy coding :-)

Another Example

Following the publication of the original article, I was asked a couple of questions about how to use this, so I wrote a more practical application example. This is buried deep in the forums now, so I have included it here for easy reference.

This example is more closely related to the base class I use which includes one or two helper functions that cater for including string length checks and loading and saving streams within your object. These methods are as follows:-

 procedure saveString(writer:TWriter;srcData:string);
  procedure loadString(reader:TReader;var dstData:string);

  procedure saveStream(writer:TWriter;src:TStream);
  procedure loadStream(reader:TReader;dst:TStream);


The exact implementations of these aren't relevant at this time, but they exist as protected methods of TStreamedObject.

You will undoubtedly notice the clunkiness of loading/saving streams. This was mentioned in the original article.

So, lets consider a simple map (TMap)... the maps height and width are variable. The map consists of 3 layers. The first layer (TGroundLayer - Descended from TTileLayer) contains the base tile for each cell, the cells movement costs. The other two layers are TTileLayer objects that simply tell the engine what tiles to draw to create overhanging trees for example.

unit unitExample;

interface

uses streamedObject, classes, SysUtils;

type
  PWord = ^Word;

  TTileLayer = class(TStreamedObject)
  protected
    fTileData : PWord;
    fWidth    : integer;
    fHeight   : integer;

    function getAddress(base:PWord;x,y:integer):PWord;

    function getTile(x,y:integer):word;
    procedure setTile(x,y:integer;value:word);
  public
    constructor create;
    destructor destroy; override;

    procedure setSize(width,height:integer); virtual;

    property tile[x,y:integer]:word read getTile write setTile;
  published
    procedure load_ver1(reader:TReader); virtual;
    procedure save_ver1(writer:TWriter); virtual;
  end;

  TGroundLayer = class(TTileLayer)
  protected
    fMovementData : PWord;

    function getMovementData(x,y:integer):word;
    procedure setMovementData(x,y:integer;value:word);
  public
    constructor create;
    destructor destroy; override;

    procedure setSize(width,height:integer); override;

    property tile;
    property movementData[x,y:integer]:word read getMovementData write setMovementData;
  published
    procedure load_ver1(reader:TReader); override;
    procedure save_ver1(writer:TWriter); override;
  end;

  TMap = class(TStreamedObject)
  protected
    fGroundLayer : TGroundLayer;
    fLayer1      : TTileLayer;
    fLayer2      : TTileLayer;
    fName        : string;

    fWidth       : integer;
    fHeight      : integer;
  public
    constructor create;
    destructor destroy; override;

    procedure setSize(width,height:integer);

    property groundLayer:TGroundLayer read fGroundLayer;
    property layer1:TTileLayer read fLayer1;
    property layer2:TTileLayer read fLayer2;

    property name:string read fName write fName;

    property width:integer read fWidth;
    property height:integer read fHeight;

  published
    procedure load_ver1(reader:TReader);
    procedure save_ver1(writer:TWriter);
  end;

implementation

(*---TTileLayer------------------------------------------------------------------------*)

function TTileLayer.getAddress(base:PWord;x,y:integer):PWord;
var
   offset : cardinal;
   temp   : PWord;
begin
     if (x>=0) and (x<fWidth>=0) and (y<fHeight) then
        begin
             offset:=y*fWidth+x;
             temp:=base;
             inc(temp,offset);

             result:=temp;
        end
     else
         raise exception.create('Coordinates out of range in '+self.classname+'.getAddress ('+intToStr(x)+','+intToStr(y)+').  Should be in the range (0..'+intToStr(fWidth-1)+',0..'+intToStr(fHeight-1)+')');
end;

function TTileLayer.getTile(x,y:integer):word;
begin
     result:=getAddress(fTileData,x,y)^;
end;

procedure TTileLayer.setTile(x,y:integer;value:word);
begin
     getAddress(fTileData,x,y)^:=value;
end;

constructor TTileLayer.create;
begin
     inherited;

     fTileData:=nil;
end;

destructor TTileLayer.destroy;
begin
     if (fTileData<>nil) then
        freeMem(fTileData);

     inherited;
end;

procedure TTileLayer.setSize(width,height:integer);
begin
     if (fTileData<>nil) then
        freeMem(fTileData);

     getMem(fTileData,(width*height*sizeOf(word)));

     fWidth:=width;
     fHeight:=height;
end;

procedure TTileLayer.load_ver1(reader:TReader);
var
   x,y  : integer;
   temp : PWord;
begin
     // You MUST ensure the size is set before attempting to
     // load the data

     temp:=fTileData;

     for y:=1 to fHeight do
         for x:=1 to fWidth do
             begin
                  temp^:=word(reader.readInteger);

                  inc(temp);
             end;
end;

procedure TTileLayer.save_ver1(writer:TWriter);
var
   x,y  : integer;
   temp : PWord;
begin
     if (fTileData<>nil) then
        begin
             temp:=fTileData;

             for y:=1 to fHeight do
                 for x:=1 to fWidth do
                     begin
                          writer.writeInteger(temp^);

                          inc(temp);
                     end;
        end
     else
         raise exception.create('Layer not initialised in '+self.className+'.save_ver1');
end;

(*---TGroundLayer------------------------------------------------------------------------*)

function TGroundLayer.getMovementData(x,y:integer):word;
begin
     result:=getAddress(fMovementData,x,y)^;
end;

procedure TGroundLayer.setMovementData(x,y:integer;value:word);
begin
     getAddress(fMovementData,x,y)^:=value;
end;

constructor TGroundLayer.create;
begin
     inherited;

     fMovementData:=nil;
end;

destructor TGroundLayer.destroy;
begin
     if (fMovementData<>nil) then
        freeMem(fMovementData);

     inherited;
end;

procedure TGroundLayer.setSize(width,height:integer);
begin
     inherited setSize(width,height);

     if (fMovementData<>nil) then
        freeMem(fMovementData);

     getMem(fMovementData,(width*height*sizeOf(word)));
end;

procedure TGroundLayer.load_ver1(reader:TReader);
var
   x,y  : integer;
   temp : PWord;
begin
     inherited load_ver1(reader);

     temp:=fMovementData;

     for y:=1 to fHeight do
         for x:=1 to fWidth do
             begin
                  temp^:=word(reader.readInteger);

                  inc(temp);
             end;
end;

procedure TGroundLayer.save_ver1(writer:TWriter);
var
   x,y  : integer;
   temp : PWord;
begin
     inherited save_ver1(writer);

     temp:=fMovementData;

     for y:=1 to fHeight do
         for x:=1 to fWidth do
             begin
                  writer.writeInteger(temp^);

                  inc(temp);
             end;
end;

(*---TMap------------------------------------------------------------------------*)

constructor TMap.create;
begin
     inherited;

     fGroundLayer:=TGroundLayer.create;
     fLayer1:=TTileLayer.create;
     fLayer2:=TTileLayer.create;

     fName:='';

     setSize(1,1);
end;

destructor TMap.destroy;
begin
     fGroundLayer.Free;

     fLayer1.free;

     fLayer2.free;

     inherited;
end;

procedure TMap.setSize(width,height:integer);
begin
     fGroundLayer.setSize(width,height);
     fLayer1.setSize(width,height);
     fLayer2.setSize(width,height);

     fWidth:=width;
     fHeight:=height;
end;

procedure TMap.load_ver1(reader:TReader);
var
   temp : TMemoryStream;
begin
     // Set the size
     setSize(reader.readInteger,reader.readInteger);

     // Read the name
     loadString(reader,fName);

     // Create a temporary stream
     temp:=TMemoryStream.create;

     // Load the stream containing the layers
     loadStream(reader,temp);

     // This is important- Return the position to 0 once you have loaded the stream
     temp.position:=0;

     // Load the layers
     fGroundLayer.loadFromStream(temp);
     fLayer1.loadFromStream(temp);
     fLayer2.loadFromStream(temp);

     // Get rid of our temporary stream
     temp.free;
end;

procedure TMap.save_ver1(writer:TWriter);
var
   temp : TMemoryStream;
begin
     // Save the size
     writer.writeInteger(fWidth);
     writer.writeInteger(fHeight);

     // Save the name
     saveString(writer,fname);

     // Create a temporary stream
     temp:=TMemoryStream.create;

     // Save the layers
     fGroundLayer.saveToStream(temp);
     fLayer1.saveToStream(temp);
     fLayer2.saveToStream(temp);

     // Save the temporary stream
     saveStream(writer,temp);

     // Get rid of our temporary stream
     temp.free;
end;

end.


A note about TGroundLayer.load_ver1 and TGroundLayer.save_ver1... this arrangement is not optimal since you will iterate through the map twice (once in the inherited load/save from TTileLayer and then once in TGroundLayers routines), however it does mean that if you add/remove data to TTileLayer you only have to update TTileLayer and the changes will be reflected without having to change the code for TGroundLayer. You could reimplement the code from TTileLayer in the TGroundLayer routines like this:-

procedure TGroundLayer.load_ver1(reader:TReader);
var
  tempTile : PWord;
  tempMove : PWord;
  x,y      : integer;
begin
  tempTile:=fTileData;
  tempMove:=fMovementData;
  for y:=1 to fHeight do
    for x:=1 to fWidth do
      begin
        tempTile^:=word(reader.readInteger);
        tempMove^:=word(reader.readInteger);

        inc(tempTile);
        inc(tempMove);
      end;
end;


This would be quicker since you only iterate through the layers data once, BUT, the two lots of data become mixed and you must then take care to ensure that the load/save routines in TGroundLayer reflect any changes you might make to TTileLayer. This is easy with only a small number of fields and a single version... imagine a lot of fields and multiple versions and you could quickly get in a mess. So, I would advise that (whilst it is not optimal from a speed point of view) you keep the data seperated into their classes as I have done in the example (this is optimal from the point of view of ease of maintenance... not to mention the OOP paradigm).

So how would you use these objects...

var
  myMap : TMap;
begin
  myMap:=TMap.create;
  myMap.name:='Test Map';
  myMap.setSize(10,10);

  myMap.groundLayer.tile[0,0]:=1;
  myMap.groundLayer.tile[0,1]:=2;
 
  // I'm sure you get the idea...


Then loading and saving your entire map is as simple as using TMap's loadFromStream and saveToStream routines.

A map that is 10 x 10 (all data set to 0) with the name 'Test Map' occupies 832 bytes on the disk. Consider that the raw data we are storing is 10 x 10 x 2 x 4 (width x height x sizeOf(word) x (ground tile + ground movement + layer 1 tile + layer 2 tile)) = 800 bytes... its pretty good at keeping the size of your data stores down because it doesn't add too much baggage. In reality if all of your map cells (tiles and movement data) were set to 256, then an approximate size would be 10 x 10 x 3 x 4 (width x height x data size x number of layers/data sets) = 1200 bytes. The reason that the data size is 3 (as stored in the stream) rather than 2 (as stored in memory) is that TReader and TWriter store a byte that indicates the size of the data and then the actual bytes that represent the data, but this is based on the actual numerical value and NOT the variable size, so 0 will always be represented by 2 bytes whether it is stored by your object as a byte, integer, word or cardinal. 255 is represented as 2 bytes, but 256 thats 3 bytes... 1 for the size and 2 for the data itself.
Valid XHTML 1.0 Transitional :: Valid CSS :: Powered by WikkaWiki
 
 
Page was generated in 0.3304 seconds