Heavy Violence
Heavy Violence

Reputation: 31

Unity Engine singleton pattern doesn't work as intended or initialization problems

dear coding community!

Currently I making my first android game with the Unity Engine 2019.4.21f1. The problem I've been fighting around for some time, seems to be completely misunderstood by me as well. It started showing up constantly when the generic singleton interacts with my own simple save/load system that I wrote because the standart PlayerPrefs are fairly restricted as to my purposes.

My generic singleton implementation:

using UnityEngine;

public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    public static T Instance { get; private set; }

    protected virtual void Awake()
    {
        foreach (T element in FindObjectsOfType<T>())
        {
            if (Instance == null)
            {
                Instance = element;
                DontDestroyOnLoad(element.gameObject);
            }
            else if (Instance != null && Instance != element) Destroy(element.gameObject);
        }
    }
}

Generic class for storing data of the same type:

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

public class GenericDataStorage<T>
{
    [SerializeField] private Dictionary<string, T> _data;

    public GenericDataStorage(params Action[] contentUpdatedHandlers)
    {
        _data = new Dictionary<string, T>();

        foreach (Action handler in contentUpdatedHandlers) ContentUpdated += handler;
    }

    private readonly Action ContentUpdated;

    public bool TryLoadValue(string key, out T value)
    {
        if (_data.ContainsKey(key))
        {
            value = _data[key];

            return true;
        }
        else
        {
            value = (T)new object();

            return false;
        }
    }

    public bool TryDeleteValue(string key)
    {
        if (_data.ContainsKey(key))
        {
            _data.Remove(key);
            ContentUpdated();

            return true;
        }
        else return false;
    }

    public void SaveValue(string key, T value)
    {
        _data.Add(key, value);
        ContentUpdated();
    }
}

Also some custom data shell class to be able to serialize all the necessary data for saving/loading with the help of JsonUtility:

using UnityEngine;

public class SavableDataShell
{
    public GenericDataStorage<float> FloatData { get; private set; }
    public GenericDataStorage<int> IntData { get; private set; }
    public GenericDataStorage<bool> BooleanData { get; private set; }
    public GenericDataStorage<GameObject> PrefabData { get; private set; }
    public GenericDataStorage<string> StringData { get; private set; }

    public SavableDataShell(GenericDataStorage<float> floatData,
                            GenericDataStorage<int> intData,
                            GenericDataStorage<bool> booleanData,
                            GenericDataStorage<GameObject> prefabData,
                            GenericDataStorage<string> stringData)
    {
        FloatData = floatData;
        IntData = intData;
        BooleanData = booleanData;
        PrefabData = prefabData;
        StringData = stringData;
    }
}

My actual database code:

using UnityEngine;
using System.IO;
using System;
using System.Text;

public class Database : Singleton<Database>
{
    private const string _FILE_NAME = "SavedGame.txt";

    public GenericDataStorage<float> FloatData { get; private set; }
    public GenericDataStorage<int> IntData { get; private set; }
    public GenericDataStorage<bool> BooleanData { get; private set; }
    public GenericDataStorage<GameObject> PrefabData { get; private set; }
    public GenericDataStorage<string> StringData { get; private set; }

    private void Save()
    {
        SavableDataShell shell = new SavableDataShell(FloatData, IntData, BooleanData, PrefabData, StringData);

        string databaseAsJson = JsonUtility.ToJson(shell);
        byte[] bytesToEncode = Encoding.UTF8.GetBytes(databaseAsJson);
        string encodedText = Convert.ToBase64String(bytesToEncode);

        string filePath = Path.Combine(Application.persistentDataPath, _FILE_NAME);

        StreamWriter sw = new StreamWriter(filePath);
        sw.Write(encodedText);
        sw.Close();
    }

    private void Load()
    {
        string filePath = Path.Combine(Application.persistentDataPath, _FILE_NAME);

        if (File.Exists(filePath))
        {
            StreamReader sr = new StreamReader(filePath);
            string encodedText = sr.ReadToEnd();
            sr.Close();

            byte[] encodedBytes = Convert.FromBase64String(encodedText);
            string decodedText = Encoding.UTF8.GetString(encodedBytes);

            SavableDataShell shell = JsonUtility.FromJson<SavableDataShell>(decodedText);

            FloatData = shell.FloatData;
            IntData = shell.IntData;
            BooleanData = shell.BooleanData;
            PrefabData = shell.PrefabData;
            StringData = shell.StringData;
        }
    }

    protected override void Awake()
    {
        base.Awake();

        FloatData = new GenericDataStorage<float>(Save);
        IntData = new GenericDataStorage<int>(Save);
        BooleanData = new GenericDataStorage<bool>(Save);
        PrefabData = new GenericDataStorage<GameObject>(Save);
        StringData = new GenericDataStorage<string>(Save);

        Load();
    }

    private void OnEnable()
    {
        Application.wantsToQuit += ApplicationWantsToQuitEventHandler;
    }

    private void OnDisable()
    {
        Application.wantsToQuit -= ApplicationWantsToQuitEventHandler;
    }

    private bool ApplicationWantsToQuitEventHandler()
    {
        Save();
        return true;
    }
}

Now the most interesting part - where the actual error occurs. It's inside the Start() method in the code below. When I trying to interact with the Database.Instance and do anything to it, it throws the following error:

NullReferenceException: Object reference not set to an instance of an object LevelDifficultyDisplay.Start()

So does all the classes that inherit from the generic singleton.

using UnityEngine;
using UnityEngine.UI;

public class LevelDifficultyDisplay : MonoBehaviour
{
    [Tooltip("A text type element to display current difficulty of the level.")]
    [SerializeField] private Text _difficultyDisplay = null;

    [Tooltip("Type an index number of a level to track its difficulty.")]
    [SerializeField] private int _level = 1;

    private void Start()
    {
        if (Database.Instance.FloatData.TryLoadValue($"level {_level} difficulty", out float d))
            _difficultyDisplay.text = string.Format("{0:###.##}", d);
        else
        {
            Database.Instance.FloatData.SaveValue($"level {_level} difficulty", 1f);
            _difficultyDisplay.text = string.Format("{0:###.##}", 1f);
        }
    }
}

I've tried to google similar errors and apply techniques that helped others, but all of them didn't work in my case. I'm sure I missing something trivial. Please, help me understand what am I doing wrong.

Cheers, Vyacheslav.

Upvotes: 1

Views: 2373

Answers (2)

Heavy Violence
Heavy Violence

Reputation: 31

Finally I've found the problem! It was JsonUtility, which docs I didn't read carefully about. Firstly, it's crucial to create and initialize a new SavableDataShell object. Then instead of JsonUtility.FromJson I must be using JsonUtility.FromJsonOverwrite to overwrite the existing object. So the right Load() method for my save/load system is the following:

private void Load()
    {
        string filePath = Path.Combine(Application.persistentDataPath, _FILE_NAME);

        if (File.Exists(filePath))
        {
            StreamReader sr = new StreamReader(filePath);
            string encodedText = sr.ReadToEnd();
            sr.Close();

            byte[] encodedBytes = Convert.FromBase64String(encodedText);
            string decodedText = Encoding.UTF8.GetString(encodedBytes);

            SavableDataShell shell = new SavableDataShell(FloatData, IntData, BooleanData, PrefabData, StringData);
            JsonUtility.FromJsonOverwrite(decodedText, shell);

            FloatData = shell.FloatData;
            IntData = shell.IntData;
            BooleanData = shell.BooleanData;
            PrefabData = shell.PrefabData;
            StringData = shell.StringData;
        }
    }

Sorry, and thank you for your time!

Upvotes: 1

Ruzihm
Ruzihm

Reputation: 20249

Your singleton does not have a way of instantiating an instance if one does not already exist, and the question does not mention that one is created manually. That combined with the null reference exception suggests that an instance does not exist.

You could either create an instance yourself, or change your Singleton to create an instance if one does not already exist. Consider the example of the Instance getter in the Unity Community Wiki Singleton, copied below:

using UnityEngine;
 
/// <summary>
/// Inherit from this base class to create a singleton.
/// e.g. public class MyClassName : Singleton<MyClassName> {}
/// </summary>
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    // Check to see if we're about to be destroyed.
    private static bool m_ShuttingDown = false;
    private static object m_Lock = new object();
    private static T m_Instance;
 
    /// <summary>
    /// Access singleton instance through this propriety.
    /// </summary>
    public static T Instance
    {
        get
        {
            if (m_ShuttingDown)
            {
                Debug.LogWarning("[Singleton] Instance '" + typeof(T) +
                    "' already destroyed. Returning null.");
                return null;
            }
 
            lock (m_Lock)
            {
                if (m_Instance == null)
                {
                    // Search for existing instance.
                    m_Instance = (T)FindObjectOfType(typeof(T));
 
                    // Create new instance if one doesn't already exist.
                    if (m_Instance == null)
                    {
                        // Need to create a new GameObject to attach the singleton to.
                        var singletonObject = new GameObject();
                        m_Instance = singletonObject.AddComponent<T>();
                        singletonObject.name = typeof(T).ToString() + " (Singleton)";
 
                        // Make instance persistent.
                        DontDestroyOnLoad(singletonObject);
                    }
                }
 
                return m_Instance;
            }
        }
    }
 
 
    private void OnApplicationQuit()
    {
        m_ShuttingDown = true;
    }
 
 
    private void OnDestroy()
    {
        m_ShuttingDown = true;
    }
}

Upvotes: 0

Related Questions