I'm getting an SerializationException from the SavingSystem

My saving system is throwing a SerializationException on save and on load. I tried a bit of debug, but I’m afraid it’s beyond me.

On Save:

SerializationException: Type 'GameDevTV.Utils.LazyValue`1[[System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]' in Assembly 'Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers (System.RuntimeType type) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.FormatterServices+<>c__DisplayClass9_0.<GetSerializableMembers>b__0 (System.Runtime.Serialization.MemberHolder _) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Collections.Concurrent.ConcurrentDictionary`2[TKey,TValue].GetOrAdd (TKey key, System.Func`2[T,TResult] valueFactory) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.FormatterServices.GetSerializableMembers (System.Type type, System.Runtime.Serialization.StreamingContext context) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo () (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize (System.Object obj, System.Runtime.Serialization.ISurrogateSelector surrogateSelector, System.Runtime.Serialization.StreamingContext context, System.Runtime.Serialization.Formatters.Binary.SerObjectInfoInit serObjectInfoInit, System.Runtime.Serialization.IFormatterConverter converter, System.Runtime.Serialization.Formatters.Binary.ObjectWriter objectWriter, System.Runtime.Serialization.SerializationBinder binder) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.Serialize (System.Object obj, System.Runtime.Serialization.ISurrogateSelector surrogateSelector, System.Runtime.Serialization.StreamingContext context, System.Runtime.Serialization.Formatters.Binary.SerObjectInfoInit serObjectInfoInit, System.Runtime.Serialization.IFormatterConverter converter, System.Runtime.Serialization.Formatters.Binary.ObjectWriter objectWriter, System.Runtime.Serialization.SerializationBinder binder) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write (System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo objectInfo, System.Runtime.Serialization.Formatters.Binary.NameInfo memberNameInfo, System.Runtime.Serialization.Formatters.Binary.NameInfo typeNameInfo) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteArrayMember (System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo objectInfo, System.Runtime.Serialization.Formatters.Binary.NameInfo arrayElemTypeNameInfo, System.Object data) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteArray (System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo objectInfo, System.Runtime.Serialization.Formatters.Binary.NameInfo memberNameInfo, System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo memberObjectInfo) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write (System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo objectInfo, System.Runtime.Serialization.Formatters.Binary.NameInfo memberNameInfo, System.Runtime.Serialization.Formatters.Binary.NameInfo typeNameInfo) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize (System.Object graph, System.Runtime.Remoting.Messaging.Header[] inHeaders, System.Runtime.Serialization.Formatters.Binary.__BinaryWriter serWriter, System.Boolean fCheck) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize (System.IO.Stream serializationStream, System.Object graph, System.Runtime.Remoting.Messaging.Header[] headers, System.Boolean fCheck) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize (System.IO.Stream serializationStream, System.Object graph, System.Runtime.Remoting.Messaging.Header[] headers) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize (System.IO.Stream serializationStream, System.Object graph) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
RPG.Saving.SavingSystem.SaveFile (System.String saveFile, System.Object state) (at Assets/Scripts/Saving/SavingSystem.cs:64)
RPG.Saving.SavingSystem.Save (System.String saveFile) (at Assets/Scripts/Saving/SavingSystem.cs:30)
RPG.SceneManagement.SavingWrapper.Save () (at Assets/Scripts/ScreenManagement/SavingWrapper.cs:57)
RPG.SceneManagement.SavingWrapper.Update () (at Assets/Scripts/ScreenManagement/SavingWrapper.cs:45)

On Load:

SerializationException: End of Stream encountered before parsing was completed.
System.Runtime.Serialization.Formatters.Binary.__BinaryParser.Run () (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize (System.Runtime.Remoting.Messaging.HeaderHandler handler, System.Runtime.Serialization.Formatters.Binary.__BinaryParser serParser, System.Boolean fCheck, System.Boolean isCrossAppDomain, System.Runtime.Remoting.Messaging.IMethodCallMessage methodCallMessage) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize (System.IO.Stream serializationStream, System.Runtime.Remoting.Messaging.HeaderHandler handler, System.Boolean fCheck, System.Boolean isCrossAppDomain, System.Runtime.Remoting.Messaging.IMethodCallMessage methodCallMessage) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize (System.IO.Stream serializationStream, System.Runtime.Remoting.Messaging.HeaderHandler handler, System.Boolean fCheck, System.Runtime.Remoting.Messaging.IMethodCallMessage methodCallMessage) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize (System.IO.Stream serializationStream, System.Runtime.Remoting.Messaging.HeaderHandler handler, System.Boolean fCheck) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize (System.IO.Stream serializationStream, System.Runtime.Remoting.Messaging.HeaderHandler handler) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize (System.IO.Stream serializationStream) (at <eae584ce26bc40229c1b1aa476bfa589>:0)
RPG.Saving.SavingSystem.LoadFile (System.String saveFile) (at Assets/Scripts/Saving/SavingSystem.cs:53)
RPG.Saving.SavingSystem.Load (System.String saveFile) (at Assets/Scripts/Saving/SavingSystem.cs:35)
RPG.SceneManagement.SavingWrapper.Load () (at Assets/Scripts/ScreenManagement/SavingWrapper.cs:62)
RPG.SceneManagement.SavingWrapper.Update () (at Assets/Scripts/ScreenManagement/SavingWrapper.cs:40)

Here is the code in my SavingWrapper;

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Saving;

namespace RPG.SceneManagement
{
    public class SavingWrapper : MonoBehaviour
    {

        const string defaultSaveFile = "save";
        
        [SerializeField] private float fadeInTime = 0.2f;

        private SavingSystem savingSystem = null;

        private void Awake()
        {
            savingSystem = GetComponent<SavingSystem>();
        }

        private void Start()
        {
            StartCoroutine(LoadLastScene());
        }

        private IEnumerator LoadLastScene()
        {
            yield return savingSystem.LoadLastScene(defaultSaveFile);
            Fader fader = FindObjectOfType<Fader>();
            fader.FadeOutImmediate();
            yield return fader.FadeIn(fadeInTime);
        }

        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.L))
            {
                Load();
            }
            
            if (Input.GetKeyDown(KeyCode.S))
            {
                Save();
            }

            if (Input.GetKeyDown(KeyCode.Delete))
            {
                DeleteSave();
            }

        }

        public void Save()
        {
            savingSystem.Save(defaultSaveFile);
        }

        public void Load()
        {
            savingSystem.Load(defaultSaveFile);
        }

        public void DeleteSave()
        {
            savingSystem.Delete(defaultSaveFile);
        }

    }

}

This is just a guess but it seems like it’s trying to serialize a class that’s not marked [System.Serializable] I’d go through all your files that should get saved and make sure any classes getting passed to the save system can be serialized.

Or maybe it’s just a broken save file and it just needs to be deleted.

I agree, and I did remove the save file when testing. Ther serialized issue was what I suspected too, and it was definitely something in the ISaveable interface in CaptureState or RestoreState (or both). I made sure all variables were using the *.value - but it did not cure the issue. I believe it’s something with the LazyValue script.
I tried making the entire thing Serializable, but I don’t fully understand this yet. I removed it and it’s dependencies within BaseStats, Health, AIController, and Fighter. I understand the need for it, but I didn’t really like implementing something I don’t understand fully.

I am getting an IndexOutOfRangeException, now that I removed LazyValue, but I feel much better debugging this, than the Serialization issue, since I think this is back to the race conditions we were trying to fix using LazyValue.

Is there a particular script you think is causing the issue? If so I could help you debug it.

Right on! I always appreciate a second set of eyes.

It’s how my Health script is calling my Progression script. I’m messing around with them right now. I added some Debug.Logs in and noticed that occasionally my GetStat is returning a 0, which throws the IndexOutOfRangeException.

When my Health.GetInitialHealth() method was being called in Awake, the race condition was very obvious with Progression building it’s index for GetStat().

What’s peculiar is that when I moved Health.GetInitialHealth() to Start, it would actually call it twice, and it’s on the second call of GetInitialHealth() that I get unexpected results. It’s strange, because the first call looks like good data received.

Screenshot 2021-04-28 125134

Screenshot 2021-04-28 125025

Screenshot 2021-04-28 124918

I then moved it to Update with a check of;

            if (!Mathf.Approximately(healthPoints, 0)) { return; }
            healthPoints = GetInitialHealth();

so it’s definitely a race condition. I have it working for now in Update… not sure if I will have issues down the road, but my saving and loading are working as expected now, and I can continue on with the course.

Here is a copy of my current Health script;

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using RPG.Saving;
using RPG.Stats;
using RPG.Core;

namespace RPG.Attributes
{
    public class Health : MonoBehaviour, ISaveable
    {
        [System.Serializable]
        public class TakeDamageEvent : UnityEvent<float>
        {
        }

        [Range(0, 100)]
        [SerializeField] private float regenHealthPercentage = 70f;
        [SerializeField] private TakeDamageEvent takeDamage;

        //private LazyValue<float> healthPoints;
        private float healthPoints = 0f;
        private bool isAlive = true;
        private BaseStats baseStats = null;

        private void Awake()
        {
            baseStats = GetComponent<BaseStats>();
            //healthPoints = new LazyValue<float>(GetInitialHealth);
        }

        private float GetInitialHealth()
        {
            Debug.Log($"{name} is returning GetInitialHealth()");
            return baseStats.GetStat(Stat.Health);
        }

        private void Start()
        {
            //if (!Mathf.Approximately(healthPoints, 0)) { return; }
            //healthPoints = GetInitialHealth();
            //healthPoints.ForceInit();
        }

        private void Update()
        {
            if (!Mathf.Approximately(healthPoints, 0)) { return; }
            healthPoints = GetInitialHealth();
        }

        private void OnEnable()
        {
            baseStats.onLevelUp += HandleLevelUp;
        }

        private void OnDisable()
        {
            baseStats.onLevelUp -= HandleLevelUp;
        }

        public bool GetIsAlive()
        {
            return isAlive;
        }

        public void TakeDamage(GameObject instigator, float damage)
        {
            print(gameObject.name + " took damage: " + damage);

            healthPoints = Mathf.Max(healthPoints - damage, 0);
            takeDamage.Invoke(damage);

            if (healthPoints > 0) { return; }

            Die();
            AwardExperience(instigator);
        }

        public float GetHealthPoints()
        {
            return healthPoints;
        }

        public float GetMaxHealthPoints()
        {
            Debug.Log($"{name} is returning GetMaxHealthPoints()");
            return baseStats.GetStat(Stat.Health);
        }

        public float GetPercentage()
        {
            return 100 * GetFraction();
        }

        public float GetFraction()
        {
            return (GetHealthPoints() / GetMaxHealthPoints());
        }

        private void Die()
        {
            if (!isAlive) { return; }

            isAlive = false;
            GetComponent<Animator>().SetTrigger("die");
            GetComponent<ActionScheduler>().CancelCurrentAction();
        }

        private void AwardExperience(GameObject instigator)
        {
            Experience experience = instigator.GetComponent<Experience>();
            if (experience == null) { return; }

            experience.GainExperience(baseStats.GetStat(Stat.ExperienceReward));
        }

        private void HandleLevelUp()
        {
            float regenHealthPoints = baseStats.GetStat(Stat.Health) * (regenHealthPercentage / 100f);
            healthPoints = Mathf.Max(healthPoints, regenHealthPoints);
        }


        #region ISaveable Interface

        public object CaptureState()
        {
            return healthPoints;
        }

        public void RestoreState(object state)
        {
            healthPoints = (float)state;
            if (healthPoints == 0)
            {
                Die();
            }
        }

        #endregion

    }

}

Here is a copy of my Progression script:

using System;
using System.Collections.Generic;
using UnityEngine;

namespace RPG.Stats
{
    [CreateAssetMenu(fileName = "Progression", menuName = "Stats/New Progression", order = 0)]
    public class Progression : ScriptableObject
    {
        [SerializeField] ProgressionCharacterClass[] characterClasses = null;

        Dictionary<CharacterClass, Dictionary<Stat, float[]>> lookupTable = null;

        public float GetStat(Stat stat, CharacterClass characterClass, int level)
        {

            BuildLookup();

            float[] levels =  lookupTable[characterClass][stat];

            if (levels.Length < level)
            {
                Debug.Log($"levels.Length < level: {levels.Length} < {level}");
                return 0f; 
            }
            Debug.Log($"stat: {stat}");
            Debug.Log($"characterClass: {characterClass}");
            Debug.Log($"levels.Length: {levels.Length}");
            Debug.Log($"level: {level}");
            Debug.Log($"level[level - 1]: {levels[level - 1 ]}");
            return levels[level - 1];
        }

        public int GetLevels(Stat stat, CharacterClass characterClass)
        {
            BuildLookup();

            float[] levels = lookupTable[characterClass][stat];
            return levels.Length;
        }

        private void BuildLookup()
        {
            if (lookupTable != null) { return; }

            lookupTable = new Dictionary<CharacterClass, Dictionary<Stat, float[]>>();

            foreach (ProgressionCharacterClass progressionClass in characterClasses)
            {
                Dictionary<Stat, float[]> statLookupTable = new Dictionary<Stat, float[]>();

                foreach (ProgressionStat progressionStat in progressionClass.stats)
                {
                    statLookupTable[progressionStat.stat] = progressionStat.levels;
                }

                lookupTable[progressionClass.characterClass] = statLookupTable;
            }
        }

        [System.Serializable]
        class ProgressionCharacterClass
        {
            public CharacterClass characterClass;
            public ProgressionStat[] stats;
        }

        [System.Serializable]
        class ProgressionStat
        {
            public Stat stat;
            public float[] levels;
        }
    }
}

I noticed in your GetStat method that it takes one off level to accommodate for the array, is it possible at any point level gets passed in as 0? Maybe have another check that will do an early return in that case?

Or maybe the level passed in is greater than the array length hence the return 0

As far as the start method getting called twice could have something to do with the saving system, if I remember correctly onload it reloads the scene so start gets called when the game starts and then again when the save system reloads the scene. That’s where the lazy value is crucial, it’s hard to say which method will get called first, the restoreState or start method, the lazy value ensures that no matter which method comes first it will be initialized properly.

For example your restoreState comes first and sets it to the value you have saved, then start gets called and overrides it to the default.

I hope some of this is helpful for you.

Yes, I think you’re right, it must be passing in a 0 for the level at some point. I did compare my Progression script to the Lecture’s Progression script and they are almost identical (crucial parts are the same). I’ve basically moved my dependent scripts back to pre-LazyValue, but I think I may try to reimplement it, starting with my Health script and comparing to the Lecture Project Changes and see if I can recreate the issue or if it magically disappears… There must be a fat finger somewhere in my code.

We have been working on the Character/Enemy prefab, so I did check to see if Starting Level was set to 0 somewhere in the prefabs, but since it’s a Range they are all at 1.

It would be awesome to have some more tools in the old debug toolkit other than Debug.Log to track down bugs. I don’t know if a debug course would be a money-maker, but I would definitely find it interesting.

I noticed that in the lectures Health script, Sam doesn’t cache the baseStats like I do in Awake()… If restoreState is able to come first, I suppose it’s possible that baseStats has not been set to GetComponent() yet.

I will use the GetComponent, rather than the cached baseStats and see if this resolves my issue.

*update:

So after removing the cached baseStats from the the different methods in my script, and then also adding LazyValue back to BaseStats.cs, it appears my issue (with Health) is resolved. Reluctantly, I wish I had taken the GIT course so I could look at my old files… now I’m second guessing my *.value check. My CaptureState may have been returning just healthpoints, not healthpoints.value…

  • update 2:

I have since returned the LazyValue to all of the routines that previously had it, and because I hate a mystery, I reimplemented the caching of baseStats in Awake(). The caching of baseStats again had no affect on the SavingSystem. So I changed my CaptureState() method to return only healthPoints (not healthPoints.value) and immediately the issue reappeared (of course)… Again, I wish I would have taken the GIT course offered here so I could go back and check my old files to be sure, but I’m going to say this was likely the fat finger that created the issue. Regardless, it was a worthwhile exercise in debugging an issues. Thanks for the help Riley_Waldo.

No problem, glad you got it solved. It’s usually stuff like this where you learn the most haha

The LazyValue issue is generally caused by returning the LazyValue itselt, not the LazyValue.value… for example… if healthPoints is a LazyValue then CaptureState should read

return healthPoints.value;

There is no good way to serialize the LazyValue itself.

Honestly, I think this was likely my issue in the beginning. I suspect I may have returned healthPoints in the CaptureState method rather than healthPoints.value. Tearing it all out and reimplementing cured the issue, so it was definitely a fat finger on my part. Silly, however, It was a good exercise in debugging.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.