Adam's Lair Forum

game development and casual madness
It is currently 2019/08/20, 15:21

All times are UTC + 1 hour [ DST ]




Post new topic Reply to topic  [ 10 posts ] 
Author Message
PostPosted: 2014/07/30, 18:45 
Member
Member

Joined: 2013/05/20, 17:19
Posts: 77
Role: Professional
When a reference to it is serialized before it is. Get it? No, neither do I:)

Trying to figure out if this is a bug or not. Looking at the PrepareWriteObject method, it seems that object references will only be serialized as ObjectRef types if the idManager has already seen this object before, but if the referenced object hasn't been serialized yet, the reference is serialized as a Struct.

This seems like a tricky problem to solve, but I was wondering if there's a mechanism in Duality's serialization to serialize referenced objects first? We were planning our save point system (actually, we've written it already, then this cropped up:) ) in such a way that it would create clones of the scene at certain points that we can just restore on retry, but that won't work if you have components that contain data directly rather than object references.

Thanks


Top
 Profile  
 
PostPosted: 2014/07/30, 20:15 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 2073
Location: Germany
Role: Professional
Actually, I'm not sure I really understand the problem yet and I'm a little confused by your posting in general, so I'll just try to answer certain segments of your posting individually:

Quote:
Looking at the PrepareWriteObject method, it seems that object references will only be serialized as ObjectRef types if the idManager has already seen this object before, but if the referenced object hasn't been serialized yet, the reference is serialized as a Struct.


Yes, that's about it. What Serialization does is start at the root object, traverse all of its Fields and sequentially serialize them, which in turn leads to a recursive operation. Now, when examining each Field / object for serialization, Value Types are written out directly while Reference Types are only written once, and that happens the first time they are encountered.

In order to prevent an object from being serialized twice, or getting stuck in a recursion circle (circular references), the idManager keeps track of each Reference Type object that is serialized. It also assigns IDs that will be used for referencing those objects when encountering them the second or third time, which is when an ObjectRef is written instead of the object itself.

You can't serialize a reference to an object, before the object itself is serialized, because, by definition, the first encounter with that object (the "reference") will automatically be the one where the actual data is written.

Quote:
This seems like a tricky problem to solve, but I was wondering if there's a mechanism in Duality's serialization to serialize referenced objects first?


There is no system the makes sure to write objects first and reference them later, because it's all done on the fly. When an object is written depends on its location in the object graph relative to the root object.

It results in XML files that are organized differently than the average human would, but on the plus side, there is no need for multi-pass serialization, which really speeds things up.

Quote:
We were planning our save point system (actually, we've written it already, then this cropped up:) ) in such a way that it would create clones of the scene at certain points that we can just restore on retry, but that won't work if you have components that contain data directly rather than object references.


Wait.. clones of the Scene? This is where I'm getting a little confused: We are talking about Serialization, right? Because Cloning and Serialization are totally different beasts. Also, I'm not sure about the part with Components containing data directly rather than referencing it?

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
PostPosted: 2014/07/30, 20:55 
Member
Member

Joined: 2013/05/20, 17:19
Posts: 77
Role: Professional
Thanks Adam. Yep, on re-reading my post, looks like I didn't make things too clear:) Sorry about that.

To address the cloning and serialization thing first, our scenes are quite big, over 6000 game objects, and serializing them takes a few seconds. When the player hits a save point in a level, we'd ideally like the save process to appear to be instantaneous, so what we do is to make a clone of the scene first. We use a custom CloneProvider class (we modified vanilla Duality a bit to support that) that makes a clone of a special game object called 'static' at init time and sticks it in the clone providers cache. Game objects that never change (which is most of them) go inside this static game object, so we only pay the cost of cloning all of those once at init time. Cloning the entire scene then takes only about 30ms. We have a plan to reduce that further by only cloning the root objects so we don't pay the cost of adding the clones to a new scene. We're currently down at about 30ms because we changed the allObj collection in GameObjectManager to a HashSet. Anyway, a little off topic. We then take the cloned scene and serialize it to a memory stream, but on a background thread so it doesn't stall the game.

We have a component called SaveProgressComponent, and that has a GameObject property that keeps track of the player game object. Sometimes by chance this component gets saved into a normal scene before the player game object, so it contains the data for the player rather than an object ref. If we run a scene like this through our save game code, I can debug through the serialization code and see that it's actually serializing the data for the player object as it was when the scene loaded, rather than saving the state as it currently is. The same problem happens to all other first occurrences of references in serialized data. Does that make sense?


Top
 Profile  
 
PostPosted: 2014/07/30, 21:28 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 2073
Location: Germany
Role: Professional
BraveSirAndrew wrote:
To address the cloning and serialization thing first, our scenes are quite big, over 6000 game objects, and serializing them takes a few seconds. When the player hits a save point in a level, we'd ideally like the save process to appear to be instantaneous, so what we do is to make a clone of the scene first. We use a custom CloneProvider class (we modified vanilla Duality a bit to support that) that makes a clone of a special game object called 'static' at init time and sticks it in the clone providers cache. Game objects that never change (which is most of them) go inside this static game object, so we only pay the cost of cloning all of those once at init time. Cloning the entire scene then takes only about 30ms. We have a plan to reduce that further by only cloning the root objects so we don't pay the cost of adding the clones to a new scene. We're currently down at about 30ms because we changed the allObj collection in GameObjectManager to a HashSet. Anyway, a little off topic. We then take the cloned scene and serialize it to a memory stream, but on a background thread so it doesn't stall the game.


You never fail to surprise me with creative ways to use Duality. That is actually pretty damn clever.

BraveSirAndrew wrote:
We have a component called SaveProgressComponent, and that has a GameObject property that keeps track of the player game object. Sometimes by chance this component gets saved into a normal scene before the player game object, so it contains the data for the player rather than an object ref. If we run a scene like this through our save game code, I can debug through the serialization code and see that it's actually serializing the data for the player object as it was when the scene loaded, rather than saving the state as it currently is. The same problem happens to all other first occurrences of references in serialized data. Does that make sense?

Hm, I'm not sure. Serialization is kind of a black box where the inputs and outputs are independent from its internal workings - the structure of the object graph remains the same, no matter in which order objects are serialized. All objects that are referenced at some point will be part of the serialized data, but it shouldn't matter where they actually are - because there is no partial serialization that allows you to load or save only certain parts of an object graph. You put an object graph in, you get an object graph out. At the end of each operation, the idManager will be cleared, so there should be no interaction between consecutive serialization operations.

It sounds like the problem was that you save a regular Scene here, and a savegame there, and whoops, now the Player ended up in the wrong one. But that shouldn't be possible, because the idManager doesn't carry over information from one operation to the next. Instead, if the Player is referenced both times, its data will be written in both operations! So it would be in both the regular Scene and the savegame.

I probably still didn't get it, because I'm relatively sure that you're already aware of all that.

However, it sounds a little bit like part of the problem you're dealing with has to do with objects (like the Player) being pulled into a serialization graph where they actually should be nothing more than references. It's just that serialization is way to greedy for that, and instead will pull every object into its process that it can get a hold of.

What you can do however, is to add a [NonSerialized] Attribute to Fields like that and re-establish the reference manually in OnInit. I'd bet that the SaveProgressComponent should only maintain a reference to the Player but never ever include its data. This would be a case where you'd slap such Attribute on the Field referencing the Player.

Was that of any help so far?

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
PostPosted: 2014/07/30, 21:59 
Member
Member

Joined: 2013/05/20, 17:19
Posts: 77
Role: Professional
Adam wrote:
You never fail to surprise me with creative ways to use Duality. That is actually pretty damn clever.


Ha, thanks:) It's a clever engine:)

Hmm, not sure. We could look up references in OnInit methods, and we totally do that in a bunch of places, but we'd have to go back through lots of code and make that change everywhere. Ugh:)

I'll try and put a small sample together that shows where the problem is because I'm finding it difficult to explain. Always easier to use the codes:)


Top
 Profile  
 
PostPosted: 2014/07/31, 12:19 
Member
Member

Joined: 2013/05/20, 17:19
Posts: 77
Role: Professional
Ok, here's a link to a sample - https://www.dropbox.com/s/ybhytmqsg9846 ... Sample.zip.

Open the scene and press play. The circle at the top (ball game object) should roll down hill, hit a save point along the way, then hit the circle at the bottom, which will cause it to reload the scene. The moving circle will then pop back to where it was when it hit the save point. Everything works, great:) The game object called SaveGameComponent has a reference to ball, and if you look at the data for the serialized scene on disk, ball was serialized first so the reference is an ObjectRef type.

Now, disable the ball game object, enable the other-ball one, and change the reference in SaveGameComponent to be other-ball. If you save that scene and look at the serialized data, because other-ball is not serialized before SaveGameComponent (for whatever reason) the reference is saved as a Struct type. All as expected, according to how serialization works.

However, if you press play on this modified scene, wait for the circle to hit the reload point at the bottom and do its reloading thing, then pause the sandbox and look at the game object reference in SaveGameComponent, you'll see that clicking the eye icon doesn't highlight the other-ball game object, but it worked when ball was the referenced game object.

So, hopefully that explains things better:)


Top
 Profile  
 
PostPosted: 2014/07/31, 16:31 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 2073
Location: Germany
Role: Professional
Thanks for your great sample! :) The behavior you're experiencing was easy to reproduce and test on, which leads me to the fact that I have both good and bad news for you:

The good one first: You have indeed found a bug and I have figured out what it is. It doesn't have anything to do with Serialization, it's an issue that has to do with Cloning. When you just remove the deep clone from the example, everything works as expected. Put it in again, things break. It also does have to do with the order in which objects are processed. And that's where the bad news starts:

There is no easy fix for this. It will potentially require large changes to the Cloning system as a whole and there is no ETA from my side when I will get around to deal with this.

The problem is rooted at the task of determining object ownership during a Cloning operation, a somewhat fundamental question: When object A is deep-cloned, and it contains references to object B, C and D, which of them belong to A (and thus need to be deep-cloned as well) and which of them are really just referenced?

When I designed the Cloning system of Duality, I have spent a lot of time figuring out how to determine the answer to that question automatically. My conclusion was that it's impossible to know just by looking at the object graph. The result is a large cascade of workarounds and manual ICloneExplicit / OnCopyTo implementations in Duality to make it work.

Now, to your issue: Because a core assumption of those workarounds is that within user Components, all object references are really just references, Duality will simply assign the Player object reference. The problem is that, in case the Player has already been cloned, that field will be assigned with a reference to the new, cloned version (because it happens to be around) - but on the other hand, when it has not already been cloned, the Player from the original Scene will be assigned (because, you know, the Component doesn't own it, so why should it clone it?).

In Serialization, something like this wouldn't be a problem, because of the "serialize everything you can get a hold on" mentality - which will allow to serializy on-the-fly and reference later. But in Cloning, this can't be done, because it would lead to a lot of unintentional clones whenever an object just references some class outside the "interesting" object graph.

I now realize that Cloning needs to be a two-pass process that determines interesting objects in the first pass and starts cloning them in the second - because in the first pass, it doesn't have the information which objects are to be cloned. However, this isn't a small change, and it will involve a lot of conceptual work and rethinking how cloning needs to work in general. The current implementation turns out to be more and more of a mess, and the next take on it needs to do better. Which is not a small task. :|

Anyway! Back to you: I have committed a new Duality version that lets Scenes do a manual prepass that scans for all GameObjects and Components first. This should fix the issue for all GameObject and Component references, but won't help for all other objects that are referenced multiple times in the object graph, but need to be cloned only once. I really hope this helps with your issue.

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
PostPosted: 2014/07/31, 22:20 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 2073
Location: Germany
Role: Professional
Quick Update: Just committed another fix, so the newly introduced Scene CopyTo prepass deals correctly with pre-cloned objects in the graph, e.g. your Static object.

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
PostPosted: 2014/08/02, 12:46 
Member
Member

Joined: 2013/05/20, 17:19
Posts: 77
Role: Professional
Thanks Adam, you super genius you:) That fix seems to have worked. Based on your excellent description of the problem, there may be some edge cases that aren't being cloned as we'd like, but I guess we'll just keep an eye out for those.

I wonder would it be worth while taking a look at how something like protobuf-net handles object ownership during deep clone operations? It's a very non-trivial problem, so it would be great if some other library had already done the heavy lifting in a compatible way.


Top
 Profile  
 
PostPosted: 2014/08/03, 11:08 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 2073
Location: Germany
Role: Professional
BraveSirAndrew wrote:
Thanks Adam, you super genius you:) That fix seems to have worked. Based on your excellent description of the problem, there may be some edge cases that aren't being cloned as we'd like, but I guess we'll just keep an eye out for those.


That's great to hear :)

BraveSirAndrew wrote:
I wonder would it be worth while taking a look at how something like protobuf-net handles object ownership during deep clone operations? It's a very non-trivial problem, so it would be great if some other library had already done the heavy lifting in a compatible way.


I'll definitely do some research and probably a lot of prototyping. Until finally fixing this, I'm extending this already existing issue to re-inventing the Cloning system.

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 10 posts ] 

All times are UTC + 1 hour [ DST ]


Who is online

Users browsing this forum: Bing [Bot] and 9 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Jump to:  
cron
Powered by phpBB® Forum Software © phpBB Group