Page 1 of 2 12 LastLast
Results 1 to 10 of 14

Thread: Blind class reconstruction (instance duplication)

  1. #1

    Blind class reconstruction (instance duplication)

    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.

    Code:
    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.

    Code:
    // 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.

  2. #2

    Blind class reconstruction (instance duplication)

    you can't do something with copymemory and instancesize?
    From brazil (:

    Pascal pownz!

  3. #3

    Blind class reconstruction (instance duplication)

    [pascal]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;[/pascal]

    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:

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

    obj := InitObject(TMyDerivedPersistentObject);[/pascal]

    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.
    Jarrod Davis
    Technical Director @ Piradyne Games

  4. #4

    Blind class reconstruction (instance duplication)

    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.
    Jarrod Davis
    Technical Director @ Piradyne Games

  5. #5

    Blind class reconstruction (instance duplication)

    Quote Originally Posted by arthurprs
    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.

  6. #6

    Blind class reconstruction (instance duplication)

    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.

  7. #7

    Blind class reconstruction (instance duplication)

    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....
    Jarrod Davis
    Technical Director @ Piradyne Games

  8. #8

    Blind class reconstruction (instance duplication)

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

    Code:
    type
      TGameObject = class
      public
        class function Duplicate: TGameObject; virtual; abstract;
        procedure Assign(From: TGameObject); virtual; abstract;
      end;
    Then adapt to my various derivative objects.

    Code:
    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:
    Code:
    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.

  9. #9

    Blind class reconstruction (instance duplication)

    Here is some working code that will showcase what I've been talking about.

    [pascal]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.
    [/pascal]

    Now a small example:

    [pascal]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.
    [/pascal]

    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.
    Jarrod Davis
    Technical Director @ Piradyne Games

  10. #10

    Blind class reconstruction (instance duplication)

    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.
    Jarrod Davis
    Technical Director @ Piradyne Games

Page 1 of 2 12 LastLast

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •