public void Step7_GameDatabase() { /* * Scenario: * We have "MyMonster" and "MyAbility" for a game. * We want to be able to easily serialize the whole graph, but we also * want MyMonster and MyAbility instances to be saved in their own files! * * Lets first take a look at the classes we're working with: */ MyMonster monster = new MyMonster(); monster.Name = "Skeleton Mage"; monster.Health = 250; monster.Mana = 100; monster.Abilities.Add(new MyAbility { Name = "Fireball", ManaCost = 12, Cooldown = 0.5f, }); monster.Abilities.Add(new MyAbility { Name = "Ice Lance", ManaCost = 14, Cooldown = 6, }); // We want to save monsters and abilities in their their own files. // Using other serializers this would be a terribly time-consuming task. // We would have to add attributes or maybe even write custom serializers so the "root objects" // can be when they are referenced in another object.. // Then we'd need a separate field maybe where we'd save a list of IDs or something.... // And then at load(deserialization)-time we would have to manually load that list, and resolve the // objects they stand for... // // And all that for literally every "foreign key" (as it is called in database terms). :puke: ! // // // Ceras offers a much better approach. // You can implement IExternalRootObject, telling Ceras the "Id" of your object. // You can generate that Id however you want, most people would proably opt to use some kind of auto-increment counter // from their SQLite/SQL/MongoDB/LiteDB/... // // At load time Ceras will ask you to load the object again given its Id. // SerializerConfig config = new SerializerConfig(); var myGameObjectsResolver = new MyGameObjectsResolver(); config.ExternalObjectResolver = myGameObjectsResolver; config.KnownTypes.Add(typeof(MyAbility)); config.KnownTypes.Add(typeof(MyMonster)); config.KnownTypes.Add(typeof(List <>)); // Ceras will call "OnExternalObject" (if you provide a function). // It can be used to find all the IExternalRootObject's that Ceras encounters while // serializing your object. // // In this example we just collect them in a list and then serialize them as well List <IExternalRootObject> externalObjects = new List <IExternalRootObject>(); config.OnExternalObject = obj => { externalObjects.Add(obj); }; var serializer = new CerasSerializer(config); myGameObjectsResolver.Serializer = serializer; var monsterData = serializer.Serialize(monster); // we can write this monster to the "monsters" sql-table now monsterData.VisualizePrint("Monster data"); MyGameDatabase.Monsters[monster.Id] = monsterData; // While serializing the monster we found some other external objects as well (the abilities) // Since we have collected them into a list we can serialize them as well. // Note: while in this example the abilities themselves don't reference any other external objects, // it is quite common in a real-world scenario that every object has tons of references, so keep in mind that // the following serializations would keep adding objects to our 'externalObjects' list. for (var i = 0; i < externalObjects.Count; i++) { var obj = externalObjects[i]; var abilityData = serializer.Serialize(obj); var id = obj.GetReferenceId(); MyGameDatabase.Abilities[id] = abilityData; abilityData.VisualizePrint($"Ability {id} data:"); } /* * Note: * * 1.) * Keep in mind that we can not share a deserialization buffer! * That means overwriting the buffer you passed to Deserialize while the deserialization is still in progress will cause problems. * "But why, when would I even attempt that??" * -> If you remember Step1 there's a part about re-using buffers. Well, in some cases you might be tempted to share a deserialization buffer as well. * For example you might think "if I use File.ReadAllBytes() for every object, that'd be wasteful, better use one big buffer and populate it from the file!" * The idea is nice and would work to avoid creating a large buffer each time you want to read an object; but when combining it with this IExternalObject idea, * things begin to break down because: * * Lets say you have a Monster1.bin file, and load it into the shared buffer. Now while deserializing Ceras realizes that the monster also has a reference to Spell3.bin. * It will send a request to your OnExternalObject function, asking for Type=Spell ID=3. * That's when you'd load the Spell3.bin data into the shared buffer, OVERWRITING THE DATA of the monster that is still being deserialized. * * In other words: Just make sure to not overwrite a buffer before the library is done with it (which should be common sense for any programmer tbh :P) * * 2.) * Consider a situation where we have 2 Person objects, both refering to each other (like the BestFriend example in Step1) * And now we'd like to load one person again. * Obviously Ceras has to also load the second person, so it will request it from you * Of course you again load the file (this time the requested person2.bin) and deserialize it. * Now! While deserializing person2 Ceras sees that it needs Person1! * And it calls your OnExternalObject again... * * > "Oh no, its an infinite loop, how to deal with this?" * * No problem. What you do is: * At the very start before deserializing, you first create an empty object: * var p = new Person(); * and then you add it to a dictionary! * myDictionary.Add(id, p); * * And then you call Ceras in "populate" mode, passing the object you created. * ceras.Deserialize(ref p, data); * * And you do it that way evertime something gets deserialized. * Now the problem is solved: While deserializing Person2 ceras calls your load function, and this time you already have an object! * Yes, it is not yet fully populated, but that doesn't matter at all. What matters is that the reference matches. * * * If this was confusing to you wait until I wrote another, even more detailed guide or something (or just open an issue on github!) * * * (todo: write better guide; maybe even write some kind of "helper" class that deals with all of this maybe?) */ // Load the data again: var loadedMonster = serializer.Deserialize <MyMonster>(MyGameDatabase.Monsters[1]); var ability1 = serializer.Deserialize <MyAbility>(MyGameDatabase.Abilities[1]); var ability2 = serializer.Deserialize <MyAbility>(MyGameDatabase.Abilities[2]); }
public void Step7_GameDatabase() { /* * Scenario: * We have "MyMonster" and "MyAbility" for a game. * We want to be able to easily serialize the whole graph, but we also * want MyMonster and MyAbility instances to be saved in their own files! * * Lets first take a look at the classes we're working with: */ MyMonster monster = new MyMonster(); monster.Name = "Skeleton Mage"; monster.Health = 250; monster.Mana = 100; monster.Abilities.Add(new MyAbility { Name = "Fireball", ManaCost = 12, Cooldown = 0.5f, }); monster.Abilities.Add(new MyAbility { Name = "Ice Lance", ManaCost = 14, Cooldown = 6, }); // We want to save monsters and abilities in their their own files. // Using other serializers this would be a terribly time-consuming task. // We would have to add attributes or maybe even write custom serializers so the "root objects" // can be when they are referenced in another object.. // Then we'd need a separate field maybe where we'd save a list of IDs or something.... // And then at load(deserialization)-time we would have to manually load that list, and resolve the // objects they stand for... // // And all that for literally every "foreign key" (as it is called in database terms). :puke: ! // // // Ceras offers a much better approach. // You can implement IExternalRootObject, telling Ceras the "Id" of your object. // You can generate that Id however you want, most people would proably opt to use some kind of auto-increment counter // from their SQLite/SQL/MongoDB/LiteDB/... // // At load time Ceras will ask you to load the object again given its Id. // SerializerConfig config = new SerializerConfig(); var myGameObjectsResolver = new MyGameObjectsResolver(); config.ExternalObjectResolver = myGameObjectsResolver; config.KnownTypes.Add(typeof(MyAbility)); config.KnownTypes.Add(typeof(MyMonster)); config.KnownTypes.Add(typeof(List <>)); // Ceras will call "OnExternalObject" (if you provide a function). // It can be used to find all the IExternalRootObject's that Ceras encounters while // serializing your object. // // In this example we just collect them in a list and then serialize them as well List <IExternalRootObject> externalObjects = new List <IExternalRootObject>(); config.OnExternalObject = obj => { externalObjects.Add(obj); }; var serializer = new CerasSerializer(config); myGameObjectsResolver.Serializer = serializer; var monsterData = serializer.Serialize(monster); // we can write this monster to the "monsters" sql-table now monsterData.VisualizePrint("Monster data"); MyGameDatabase.Monsters[monster.Id] = monsterData; // While serializing the monster we found some other external objects as well (the abilities) // Since we have collected them into a list we can serialize them as well. // Note: while in this example the abilities themselves don't reference any other external objects, // it is quite common in a real-world scenario that every object has tons of references, so keep in mind that // the following serializations would keep adding objects to our 'externalObjects' list. for (var i = 0; i < externalObjects.Count; i++) { var obj = externalObjects[i]; var abilityData = serializer.Serialize(obj); var id = obj.GetReferenceId(); MyGameDatabase.Abilities[id] = abilityData; abilityData.VisualizePrint($"Ability {id} data:"); } // Problems: /* * 1.) * Cannot deserialize recursively * we'd overwrite our object cache, Ids would go out of order, ... * Example: A nested object tells us "yea, this is object ID 5 again", while 5 is already some other object (because its the wrong context!) * * -> Need to make it so the serializer has Stack<>s of object- and type-caches. * * * 2.) * Keep in mind that we can NOT share a deserialization buffer!! * If we load from Monster1.bin, and then require Spell5.bin, that'd overwrite our shared buffer, * and then when the spell is done and we want to continue with the monster, the data will have changed! * * -> debug helper: "The data has changed while deserializing, this must be a bug on your end!" * * 3.) * while deserializing objects, we need to create them, add to cache, then populate. * otherwise we might get into a situation where we want to load an ability that points to a monster (the one we're already loading) * and then we end up with two monsters (and if they code continues to run, infinite, and we get a stackoverflow) * In other words: Objects that are still being deserialized, need to already be in the cache, so they can be used by other stuff! * * -> create helper class that deals with deserializing object graphs? * */ // Load the data again: var loadedMonster = serializer.Deserialize <MyMonster>(MyGameDatabase.Monsters[1]); var ability1 = serializer.Deserialize <MyAbility>(MyGameDatabase.Abilities[1]); var ability2 = serializer.Deserialize <MyAbility>(MyGameDatabase.Abilities[2]); }