Extending the JsonSavingSystem

I am faced with a major design decision for my game, where I need to “shapeshift” or just change between certain characters. One of the main problems that I have is that for some characters, I would like to keep certain things “shared” such as stats, inventory, equipment, etc.

Due to how my character controller works, and how things are being setup, i can’t just swap out meshes or simply swap the prefabs and call it a day.

I then remembered that most things that I want to “share” are things that are being saved with the JsonSavingSystem, so I thought why not leverage this and somehow save the state of the character i switching from to the new character.

But this would require some changes, because I might only want to do this for certain “forms”.

Here is an example of how my TransformableCharacter class looks like:

[Serializable]
    public class TransformableCharacter
    {
        public CharacterType characterForm;
        public CharController charInstance;
        public bool isActive;
        public bool shouldShareState;
        public string sharedStateID;

        public TransformableCharacter(CharController char, CharacterType form, bool shareState, string stateID)
        {
            characterForm = form;
            charInstance = char;
            isActive = false;
            shouldShareState = shareState;
            sharedStateID = stateID;
        }
    }

I added the shouldShareState bool and the sharedStateID to show what i meant by “only certain forms” should share a state.
So, in the Swap method, I would do a check on the character to see if it should share state, and if it does then i would capture that forms state, as you would when you save the game. The tricky part is that, the save system takes in a UUID, that sits on that JsonSaveableEntity. What I need the new extension to do is to somehow have a different method that will capture the state but i would like it to use the sharedStateID to do that. Because I can’t have multiple prefabs in the scene that share the same uniqueIdentifier, like Player for example. IT would have been easier if it could.

But the idea would be to check if the form should share the state, if ti does, the save it, ideally i would like to save this as a cache, and not on file, for example do not write these values to file like the save system normally does, but to have a local cache:

private Dictionary<string, JToken> characterStateCache = new Dictionary<string, JToken>();

and just save those states there, because the idea would be that when i call the Swap method, it will do the check to see if the oldcharacter shouldShareState, if true, then CaptureState, and when i transform to the newCharacter, then i also check if it shouldShareState, and if it does, then i would do a RestoreState, from the local cache.
I think here is where i would use the sharedStateID field, where it shouldn’t necessarily be unique, because if let’s say i want to save the state between 2 humanoid forms that would share the inventory and equipment and stats, then i would use the same sharedStateID for those forms. I guess an extra check could be made to see if the newcharacter.sharedStateID is the same as the oldcharacter.sharedStateId, then I know that i can do a restoreState on the newCharacter using the sharedStateID to grab the state from the cache.

This should cover the issue i have with saving and loading the states at runtime between 2 characters, but then I would have an issue when I will actually Save the game, because while the local cache would keep the current state of the characters i am swapping from, where basically the 1st time i shape shift, it will carry over the 1st characters stats, then when i swap back to it, it will carry over the new state, meaning if when i swapped over the 1st char, i had lvl 10 and 1 item in the inventory, this would transfer over to the 2nd character, i would then proceed to level to 20 and have 3 items in the inventory and 1 in the equipment slot, and when i would switch back over to the 1st character, all these new change would basically be copied to it, so it would also be lvl 20 with 3 items in inv and 1 equipped.

Now the main concern i have is when i will save the game, if the 1st character will level to lvl 30 now, and i never to a swap back to the 2nd char so it would also have a copy of the new state, when i save, the prefab for the 2nd character will save with lvl 20, 3 items, 1 equipped, etc. When i will load the game, i would basically load in with the 1st character loaded, so if i swap to the 2nd char, then it would have the new state, but the issue would be, if i continue to play with the 2nd character, get to lvl 40 (1st char is lvl 30), then i save the game on the 2nd char and quit, when i load in i will basically be on the 1st character loaded, and it would have the old lvl 30 state, etc, so you can see where my issue is.

I remember that Brian also helped out with a nice addition to the JsonSavingSystem where you can set a character to be isManaged bool, and put that character in a managed characters list on the main prefab, so that even if a character prefab is disabled when you perform a save, it will still save its state, so that it can be loaded as well.
I was wondering if that could also be used with this new extension?
But the main concern is still that scenario where you play on the second character and save and exit and when you load back, you are on the 1st character and if you ever switch to the 2dn character you would basically loose the “shared” progress you did on it.

I am still trying to figure out a way on how to make this work, when playing multiple character, because of how the dialogue system would work, the shop system and the predicate system works.

Thanks!

I found this thread Procedurally Spawned Characters and i took the idea from the DynamicEntity.cs of the capture and restore state and implemented it in my ShapeShiftManager, it looks something like:

public JToken CaptureAsJToken()
        {
            JObject state = new JObject();
            foreach (IJsonSaveable saveable in _currentActiveCharacter.animalInstance.gameObject
                         .GetComponents<IJsonSaveable>())
            {
                 state[saveable.GetType().ToString()] = saveable.CaptureAsJToken();
            }

            return state;
        }

        public void RestoreFromJToken(JToken state)
        {
            if (state is JObject stateDict)
            {
                foreach (IJsonSaveable saveable in _currentActiveCharacter.animalInstance.gameObject
                             .GetComponents<IJsonSaveable>())
                {
                    
                    string typeString = saveable.GetType().ToString();
                     if (stateDict.ContainsKey(typeString))
                     {
                         saveable.RestoreFromJToken(stateDict[typeString]);
                     }
                }
            }
        }

        private void CacheCharacterState(TransformableCharacter character)
        {
            if (!character.shouldShareState) return;
            JToken characterState = CaptureAsJToken();
            _characterStateCache[character.sharedStateID] = characterState;
        }

        private void RestoreCharacterStateFromCache(TransformableCharacter character)
        {
            if (!character.shouldShareState) return;

            if (_characterStateCache.TryGetValue(character.sharedStateID, out JToken characterState))
            {
                RestoreFromJToken(characterState);
            }
        }

What I am doing is am specifying the current active character when i do .GetComponents for IJsonSaveable.

This way, right before i swap my character, i create a jtoken state and cache it in the dictionary, then at the end of the swap, i perform the restore.

At first glance, this work, ok, but there are many edge cases, specifically the one that i mention where you play as the second character save, and when you continue you are on the main, default character and the state of the second character was not saved.

Another issue comes from the ItemDropper, where it saved all the items you dropped. Doing a state save and load will, inadvertently duplicate all dropped items, something which is not ideal.
I have yet to think of a solution on how to make this work, properly. By trying to somehow manage the state between the two characters when performing a real manual save, so that the cached state could apply to both character prefabs, or any number of prefabs that would need to share states.

Another issue would be the performance of this. I found out later on that switching forms too many times, would create lag spikes because of the saving and loading sates, and i don’t know if there would be a way to have that fixed.

It might help to put the specification into more of an outline or mindmap format, as this will inform the best way to accomplish this…
As long as we’re using one file to represent the totality of saves, we’re going to be stuck with ensuring that when you save the 1st character, the 2nd character (or 3rd, etc) is also wrapped up with the 1st character in the save.
One thing we can do is decouple certain classes from the characters… For example: All of the characters will be sharing the same Inventory, Equipment, and Experience components. A parent object could contain the common classes, while various child objects (each JsonSaveableEntities in their own right) would contain the classes that are individual.

I suspect, however, that more classes will be in this superobject than you might think… As this is still essentially the same character switching between forms, in addition to inventory, equipment and experience, they probably will also share the same QuestList component, and possibly the same Traits (though that might be an interesting one to keep separate… In my normal Sayan form, I have high intelligence, but in my Super Sayan form, my Strength is higher, etc…

So let’s start with this: Go through each component on the player(s). Make a list of which components are shared, and which components are individual (either because they may not appear on a transformation, or because they would contain different values).

From there, changes on how the different components interact will need to be addressed.

I also thought of a different way, where i would have the “shareable” components on a PlayerManager gameobject, and then the actual player gameObjects would be independent. But then i realized that accessing the components on the PlayerManager would be a bit challenging since for example, for combat, when the player would get hit, anything that would happen with GetComponent on the player prefab, would not have access of the Health, or any other component sitting on the PlayerManager object.

That’s why i needed a different approach, otherwise i would have to go everywhere where i would need to get a component that would be on the player prefab, check if it’s a “Player” and then replace it with the PlayerManager.Instance.gameobject, to access the components i need.

As far as which components i would like to share, here is a list with ones that you would be familiar from the course, i have many others that are custom, but it shouldn’t be an issue.

Inventory
Equipment,
BaseStats,
TraitStore,
Experience,
Health,
Mana,
CooldownStore,
QuestList,
ItemDropper (maybe? this will cause some duplicate items problems when loading the state, so that would need to be taken care of)

These components would matter the most, when it would come to combat interactions or NPC interactions, i think.
Of course i would need to go and redo how i get references for any UI component, since those get cached in awake, by searching for the player tag. as well as NPC like shop, conversant. I would need to have an event to trigger a re-cache for the components every time i change forms.

I managed to “filter” which components get saved by using:

if (saveable is Inventory || saveable is QuestList)

Seems to work ok, but i still have to see if there will be performance issues, if you switch forms too many times during 1 play session.

Privacy & Terms