Robert has asked for a bigger example of how to use the basic data store from my article. The example code below will hopefully give you a better idea of how it can be used.
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:-
Code:
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 (refer to the article - page 3 - for the example code I provided).
You will undoubtedly notice the clunkiness of loading/saving streams. This is mentioned in the 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.
Code:
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
begin
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;
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
begin
freeMem(fTileData);
end;
inherited;
end;
procedure TTileLayer.setSize(width,height:integer);
begin
if (fTileData<>nil) then
begin
freeMem(fTileData);
end;
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
begin
for x:=1 to fWidth do
begin
temp^:=word(reader.readInteger);
inc(temp);
end;
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
begin
for x:=1 to fWidth do
begin
writer.writeInteger(temp^);
inc(temp);
end;
end;
end
else
begin
raise exception.create('Layer not initialised in '+self.className+'.save_ver1');
end;
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
begin
freeMem(fMovementData);
end;
inherited;
end;
procedure TGroundLayer.setSize(width,height:integer);
begin
inherited setSize(width,height);
if (fMovementData<>nil) then
begin
freeMem(fMovementData);
end;
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
begin
for x:=1 to fWidth do
begin
temp^:=word(reader.readInteger);
inc(temp);
end;
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
begin
for x:=1 to fWidth do
begin
writer.writeInteger(temp^);
inc(temp);
end;
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:-
Code:
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
begin
for x:=1 to fWidth do
begin
tempTile^:=word(reader.readInteger);
tempMove^:=word(reader.readInteger);
inc(tempTile);
inc(tempMove);
end;
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...
Code:
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.
Hope this helps clarify the usage of the base class.
Bookmarks