Esempio n. 1
0
        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]);
        }
Esempio n. 2
0
        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]);
        }