PDA

View Full Version : Blind class reconstruction (instance duplication)



Robert Kosek
28-03-2008, 01:58 PM
I'm working on a game project, and I need to blindly be able to duplicate a class. Because Pascal cannot do this natively, what I'm doing is writing code for Assign so that I can assign from the original to the new. The usage of this would be for store inventory to a player's inventory in a RPG.

For instance, here's my Assign event for the base TItem class.


procedure TItem.Assign(From: TItem);
begin
fCopied := True;
fName := From.Name;
fDesc := From.Description;
fIntName := From.InternalName;
fValue := From.Value;
// No idea if the following will work!
fAttr[0] := From.fAttr[0].ClassType.Create;
fAttr[1] := From.fAttr[1].ClassType.Create;
fAttr[0].Assign(From.fAttr[0]);
fAttr[1].Assign(From.fAttr[1]);
end;

fAttr is a defined as: "array[0..1] of IAttribute". This sort of thing would be if you get a smith to harden your sword, you'd gain the attribute and it'd apply its bonus(es) to the item. Only trouble is, as you can see, I'm struggling to figure out if this sort of blind creation will work. Every Attribute, when I get them written, will have a Create call with no parameters and will be assigned to from the attribute derived from.

Ick, this is more complex than I had thought. I'll just post the source (it is all untested!).

What do you all think? Will this work, and if it won't then how can I fix it? I really need some kind of way to make a blind construction system like this.


// This source is private source code, please don't reuse it in your projects
// without my express written permission.
unit uWeapon;

interface

type
TItem = class;

// An attribute is a bonus to a given item.
IAttribute = interface
procedure ApplyTo(What: TItem);
procedure Assign(From: IAttribute);
end;
IAttributePair = Array[0..1] of IAttribute;

// Placeholder class for a real inventory item class
TItem = class
private
fCopied: Boolean;
fName, fDesc, fIntName: String;
fValue: Single;
fAttr: IAttributePair;
function GetAttr(Index: Boolean): IAttribute;
procedure SetAttr(Index: Boolean; Value: IAttribute);
public
property Name: String read fName write fName default 'Unknown';
property Description: String read fDesc write fDesc;
property InternalName: String read fIntName write fIntName;
property Value: Single read fValue write fValue default 0.00;
// Indexer...
property Attributes[Index: Boolean]: IAttribute read GetAttr write SetAttr;
// Constructor/destructor...
constructor Create;
destructor Destroy;
// Basic functions...
procedure Assign(From: TItem);
end;

implementation

constructor TItem.Create;
begin
fCopied := False;
fName := 'Unknown';
fDesc := 'The mysterious unknown item!';
fIntName := 'Item, Unknown';
fValue := 0.00;
fAttr := [nil, nil];
end;

destructor TItem.Destroy;
begin
if Assigned(fAttr[0]) then
FreeAndNil(fAttr[0]);
if Assigned(fAttr[1]) then
FreeAndNil(fAttr[1]);

inherited Destroy;
end;

procedure TItem.Assign(From: TItem);
begin
fCopied := True;
fName := From.Name;
fDesc := From.Description;
fIntName := From.InternalName;
fValue := From.Value;
// No idea if the following will work!
fAttr[0] := From.fAttr[0].ClassType.Create;
fAttr[1] := From.fAttr[1].ClassType.Create;
fAttr[0].Assign(From.fAttr[0]);
fAttr[1].Assign(From.fAttr[1]);
end;

function TItem.GetAttr(Index: Boolean): IAttribute;
begin
Result := fAttr[Ord(Index)];
end;

procedure TItem.SetAttr(Index: Boolean; Value: IAttribute);
begin
if (Value := nil) and Assigned(fAttr[Ord(Index)]) then
FreeAndNil(fAttr[Ord(Index)])
else
fAttr[Ord(Index)] := Value;
end;

end.

arthurprs
28-03-2008, 06:12 PM
you can't do something with copymemory and instancesize?

Pyrogine
28-03-2008, 06:16 PM
TPersistentObject = class
public
constructor Create; virtual;
destructor Destroy; override;
procedure Save(aStream: TStream); virtual;
procedure Load(aStream: TStream); virtual;
procedure Assign(var aObj: TPersistentObject); virtual;
end;

TPersistentObjectClass = class of TPersistentObject;

Now you can can have a system where you can register you objects for persistence. Since TPersistentObject has a virtual constructor you can do stuff like this:

function InitObject(aObj: TPersistentObjectClass): TPersistantObject;
begin
Result := aObj.Create; // works because of the virtual constructor
end;

obj := InitObject(TMyDerivedPersistentObject);

My point is that by having a persistent object and taking advantage of virtual constructors you can create instances of objects without full knowledge of them. If you had a bunch of objects saved to a stream, you would be able to load them all back into memory.

How can this help you with your problem? Assuming that you have a good hierarchy of streamable classes prepared and registered with your streaming system, to duplicate any persistent class would simply be a matter of saving to a temp stream, then creating an instance of this class off the stream and it would be duplicated without knowledge of the classes that it's derived from. The streaming system helps to make it less complex moving forward.

I hope you can see the underlying logic of my point. Inside the Assign method you create a temp stream, save the object off to it, then create an instance of it and assign it to aObj. Now no matter how complex it maybe, if you've setup things where the object can save/load itself from the stream it will be recreated. Plus you have the advantage of a general object persistence system.

In one project I was able to save all the game objects to a stream (including objects that happen to be exploding at the time) and was able to load all of them back and those objects continued exploding. That was freakishly cool.

Now you have to decide on how complex you want your streaming system. In my case I manually saved all the relevant data to and from the stream from inside the Save/Load methods. You can have things where you can define which fields gets saved and loaded so that it's more automatic sort of like what Delphi does with published properties. I just needed the framework in place and when Save/Load gets called either directly by me or from some another class you just have to make sure the proper data will get saved and can be loaded. It worked really well for me in the past and I will be using this system for PyroGine SDK.

Maybe this can be a possible solution for you as well.

Pyrogine
28-03-2008, 06:41 PM
Or...

If you're not interested in a general streaming system, as long as your objects can save/load themselves to a stream (memory, file or whatever) then you should still be able to do what you want to do. If you have Load/Load/Assign virtual methods and each derived class also calls the inherited Save/Load methods, it still should be able to work.

Inside Assign, have the object save itself to the stream, then have the object that you wish to be assigned, call it's load method, passing in the saved stream pointer and it will be duplicated.

Robert Kosek
28-03-2008, 07:02 PM
you can't do something with copymemory and instancesize?Not that I am aware of. Personally, I don't think that it would be wise to try this. :?

Jarrod:

I see what you say, and that form of system is very beneficial in a game framework. However, I am uncertain as to how I should implement this. Firstly, as you can see, my unit is woefully small and very spartan in features. What I am working with is an attempt to design a framework for inventory items in a RPG.

To sidetrack, slightly, I must confess that at OOP my skills are below average. I am good at writing classes, that's not the point. My difficulty is in tying everything together well and not the classes themselves.

What I am trying to do is this:
An item can have up to two bonus attributes. If a sword is particularly stout then it would have an attribute derived from the base Visitor model, the IAttribute interface, and it would then visit its parent object. This is not the trouble, currently, as I find this to be simple enough to implement on my own. The trouble is in purchasing.

Lets say there's a blacksmith with that longsword you've been eying for a few hours. It's stout and is well balanced, making it tough and easy to hit with. However, the smith has three of these items. In the data these would derive from a "TStackableItem" class where, in memory, there is only one item with a count of three. If I aim to buy this I cannot strictly duplicate the item and its attributes as a pointer, as the visitor attributes could change -- what if a wizard cursed the sword? -- at any moment and so must be unique to an item. I could easily enough say fItem.ClassType (a pointer to the class of class type) and call Create, then Assign the properties from the store's sword to the player's inventory. The trouble are the attributes, because I could do this too -- and I highly doubt that it works.

Unfortunately, all my ideas on how to duplicate and assign properties are all hypothetical. Because I am working with FPC there are a few inconsistencies with Delphi, without compatibility turned on, but there are usually parallel traits or at least a work around.

Would you help me plan out this series of classes? I am struggling with this and can't seem to wrap my head about all the classes there will be and how to make them duplicable, yet stackable, and containing their own unique traits. It's a wee bit complicated for someone inexperienced in the finer arts of OOP.

Far simpler to botch this than it is to do it right, unfortunately.

Brainer
28-03-2008, 07:13 PM
I think it's a bit off-topic, but you should read the book "Programming Role-playing games with DirectX 8.0". It explains a lot.

Pyrogine
28-03-2008, 09:06 PM
Ok, lets see here....

When designing complex systems, it is often preferable to break things down into smaller and smaller interconnecting parts until they become "atomic". This is the point at which the problem can not be broken down any further and this part has to exist (irreducible) in order for the system to function. Sometimes this "atom" can itself be complex (irreducible complexity).

So by "atomizing" the large complex problem we then get a series of smaller goals that we need to achieve that are less dependent on each other and thus more manageable. They can be thoroughly debugged and more or less be forgotten about. This is what we are after. If were to go with the approach that an object can completely save/load itself than any object derived from this base object type can completely save/load itself no matter how complex it may become. Because this "atomic" part just works it removes itself from the overall complexity of the solution. If we discover a bug in this area and fix it, it is more or less guaranteed to work correctly all the way up the chain.

At this point you no longer have to think about how objects are stored, just the fact that they can save/load themselves. If I can save/load myself I can easily be assigned to another object of the same type or derived from the same type.

Let look at this:

Say I have ObjectA and ObjectB which is derived from ObjectA. So ObjectB would be a superset of ObjectA with additional data and both objects "know" how to save/load themselves. if I assign ObjectB to ObjectA then we need to be able to call the inherited method of ObjectB that can save the ObjectA level data to the stream. I have to do some test to see how to figure out which inherited method to call based on the derived object passed in. But my point here is that if we can get this working correctly then you should be able to assign one object to another and it will copy the data it has in common. At present it will be able to duplicate itself if you pass in the same type. I just need to figure out a way to save the parts that they have in common for a complete object assignment solution.

I know on the surface this seems more completed than it should be, but it's really not. The complication at this point is in thoroughly defining our goal first of all and over coming any limitations imposed by the compiler to archive this goal. I wanted to attack the problem from a software engineering perspective so that we can apply what's been learned going forward.

Ahhhh.. brain hurts gotta do some more thinking....

Robert Kosek
28-03-2008, 11:45 PM
Okay. I guess we're pretty much on the same page, because atomization is what I've been trying to accomplish the whole time. My difficulty is in puzzling out the atomization but not understanding the process.

I think I'll base it around a common base game object (abstract of course).


type
TGameObject = class
public
class function Duplicate: TGameObject; virtual; abstract;
procedure Assign(From: TGameObject); virtual; abstract;
end;

Then adapt to my various derivative objects.


type
TItem = class(TGameObject, ISerialize)
private
// All the properties and variables, etc....
public
class function Duplicate: TGameObject; virtual;
procedure Assign(From: TGameObject); virtual;
procedure Save(Stream: TStream);
procedure Load(Stream: TStream);
end;

TStackedItem = class(TItem);

TWeapon = class(TItem);

TAttribute = class(TGameObject, ISerialize)
private
// All the properties and variables, etc....
// Plus storage for the amounts given,
// taken, etc to make revocation simple.
public
class function Duplicate: TGameObject; virtual; override;
procedure Assign(From: TGameObject); virtual; override;
procedure Save(Stream: TStream);
procedure Load(Stream: TStream);
// Unique:
procedure ApplyTo(What: TWeapon);
procedure Revoke(From: TWeapon);
end;

I think that this would make things simpler. For instance, I could do this:
class function TItem.Duplicate: TGameObject;
begin
Result := TItem.Create;
Result.Assign(Self);
end;

procedure TItem.Assign(From: TGameObject);
begin
if not (From is TItem) then
raise Exception.CreateFmt('Error! Cannot assign to a TItem from a %s.', [From.ClassName])
else begin
// reset any properties that are objects...
// and then apply any settings from the FROM object...
// and duplicate any TGameObject descended classes needed.
end;
end;

This is currently pseudocode and errant ramblings on the subject, but I think my brain is slowly straightening these things out. Thankfully if it is, then it is all subconsciously--because my head hurts less this way. I feel more confident about it at least. Obviously there is still much to consider, test, and work out. I think this is a decent start to planning at least.


Brainer: It may be helpful, but I am only after designing the OOP framework, the OOA&D portions, and not the rest about DX8 or DX9 and the rest of the engine stuff. Once I get the basic data types down and all the interdependencies worked together, then I can easily make myself an engine. Dunno what rendering framework I'll do it with yet, but it may just be ASCII. I am quite far from that point in time just now.

Pyrogine
29-03-2008, 02:52 AM
Here is some working code that will showcase what I've been talking about.

unit uStreamObjects;

interface

uses
SysUtils,
Classes;

type

{ TStreamObject }
TStreamObject = class
public
constructor Create;
destructor Destroy; override;
procedure Save(aStream: TStream); virtual;
procedure Load(aStream: TStream); virtual;
function Size: Integer; virtual;
procedure Assign(aObj: TStreamObject); virtual;
end;

{ TObjectA }
TObjectA = class(TStreamObject)
protected
FData1: Integer;
public
constructor Create;
destructor Destroy; override;
procedure Save(aStream: TStream); override;
procedure Load(aStream: TStream); override;
property Data1: Integer read FData1 write FData1;
end;

{ TObjectB }
TObjectB = class(TObjectA)
protected
FData2: Integer;
public
constructor Create;
destructor Destroy; override;
procedure Save(aStream: TStream); override;
procedure Load(aStream: TStream); override;
property Data2: Integer read FData2 write FData2;
end;


implementation

{ --- TStreamObject ----------------------------------------------------}
constructor TStreamObject.Create;
begin
inherited;
end;

destructor TStreamObject.Destroy;
begin
inherited;
end;

procedure TStreamObject.Save(aStream: TStream);
begin
end;

procedure TStreamObject.Load(aStream: TStream);
begin
end;

function TStreamObject.Size: Integer;
var
stm: TMemoryStream;
begin
stm := TMemoryStream.Create;
try
Save(stm);
Result := stm.Size;
finally
stm.Free;
end;
end;

procedure TStreamObject.Assign(aObj: TStreamObject);
var
Stream: TMemoryStream;
begin
// create memory stream
Stream := TMemoryStream.Create;

try
// check if incoming object is larger
if aObj.Size > Self.Size then
begin
// save to stream
aObj.Save(Stream);

// reset stream position
Stream.Position := 0;

// load in data from stream
Self.Load(Stream);
end
else
begin
// save self data to stream
Self.Save(Stream);

// reset stream position
Stream.Position := 0;

// write incoming data to stream
aObj.Save(Stream);

// reset stream position
Stream.Position := 0;

// load in data from stream
Self.Load(Stream);
end;
finally
Stream.Free;
end;
end;

{ --- TObjectA ---------------------------------------------------------}
constructor TObjectA.Create;
begin
inherited;
FData1 := 0;
end;

destructor TObjectA.Destroy;
begin
inherited;
end;

procedure TObjectA.Save(aStream: TStream);
begin
inherited;
aStream.Write(FData1, SizeOf(FData1));
end;

procedure TObjectA.Load(aStream: TStream);
begin
inherited;
aStream.Read(FData1, SizeOf(FData1));
end;


{ --- TObjectB ---------------------------------------------------------}
constructor TObjectB.Create;
begin
inherited;
FData2 := 0;
end;

destructor TObjectB.Destroy;
begin
inherited;
end;

procedure TObjectB.Save(aStream: TStream);
begin
inherited;
aStream.Write(FData2, SizeOf(FData2));
end;

procedure TObjectB.Load(aStream: TStream);
begin
inherited;
aStream.Read(FData2, SizeOf(FData2));
end;

end.


Now a small example:

var
A: TObjectA;
B: TObjectB;

begin
A := TObjectA.Create;
B := TObjectB.Create;

A.Data1 := 1;

B.Data1 := 2;
B.Data2 := 3;

WriteLn('A.Data1: ', A.Data1);
WriteLn('B.Data1: ', B.Data1);
WriteLn('B.Data2: ', B.Data2);


A.Assign(B);
writeln('after assignment...');
WriteLn('A.Data1: ', A.Data1);
WriteLn('B.Data1: ', B.Data1);
WriteLn('B.Data2: ', B.Data2);


B.Free;
A.Free;

Write('Press ENTER to continue...');
ReadLn;
end.


A small situation you will need to decide how to handle is the case where B is smaller than A. The data in A beyond B becomes essentially undefined because there maybe a situation where A's data is directly dependent on inherited data which is often the case. Ideally you would clear all the fields of A, do the assignment and then reinit all dependent data moving down the chain.

The assign method presented here will simply try to figure out which object is largest and write that out to the stream first, then over write the stream with the new data and then read this updated stream back into.

Keep me posted on your progress.

Pyrogine
29-03-2008, 03:09 AM
The nice thing about the type of solution is that it works at higher levels of complexities. As long as your objects can properly save/load then it should continue work. The stream flattens out the data which makes it easy to move then the object reads it back and figures out what to do with it. This opens up all sorts of possibilities for future growth. If you extend this a bit more you can have a general persistent framework that can be used in all your projects. Hmm.. just some random thoughts.

This has been a good exercise as it has help me as well to define some things that's been on my to-do list. Coolness.

Robert Kosek
31-03-2008, 03:47 PM
Sorry Jarrod, you're talking apples to my oranges. We both understand streams well, and this doesn't solve my issue (nor even address it). Serialization will not reach across types where an assign call that copies data will--serialization in my case will lead to numerous errors that I wish to avoid.

Does anyone else have insight into my situation?

Pyrogine
31-03-2008, 04:40 PM
Robert, check your PM please.

Robert Kosek
01-04-2008, 01:44 AM
Oh golly, I really should have thought of this earlier! :doh: All I have to do is use a Decorator class to wrap transform an item into a stack, and if duplication is already handled it can easily handle addition and subtraction of items from the stack.

That solves that problem, and prevents any need for a "TStackableItem" class, as any base item could be stacked. Goodie. :D

I'll have to write a few tests and see how well I can do blind class duplication. This could get interesting really quickly.

arthurprs
01-04-2008, 06:37 PM
i loved this emoticon :doh: :doh:
:lol: