Unity 引擎单例模式无法按预期工作或出现初始化问题

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

亲爱的编码社区!

目前我正在使用 Unity Engine 2019.4.21f1 制作我的第一个 android 游戏。我已经争论了一段时间的问题,似乎也被我完全误解了。当通用单例与我自己编写的简单 save/load 系统交互时,它开始不断出现,因为标准 PlayerPrefs 在我的目的方面受到相当限制。

我的通用单例实现:

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);
        }
    }
}

通用class,用于存储相同类型的数据:

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();
    }
}

还有一些自定义数据 shell class 能够在 JsonUtility 的帮助下为 saving/loading 序列化所有必要的数据:

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;
    }
}

我的实际数据库代码:

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;
    }
}

现在是最有趣的部分 - 实际错误发生的地方。它位于下面代码中的 Start() 方法中。当我尝试与 Database.Instance 交互并对其执行任何操作时,它会抛出以下错误:

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

从通用单例继承的所有 classes 也是如此。

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);
        }
    }
}

我曾尝试 google 类似的错误并应用对他人有帮助的技术,但所有这些都不适用于我的情况。我确定我遗漏了一些微不足道的东西。请帮助我理解我做错了什么。

干杯,维亚切斯拉夫。

如果一个实例不存在,你的单例没有办法实例化,而且问题没有提到一个是手动创建的。结合空引用异常表明实例不存在。

您可以自己创建一个实例,或者更改您的 Singleton 以创建一个实例(如果实例不存在)。考虑 Unity Community Wiki SingletonInstance getter 的示例,复制如下:

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;
    }
}

终于找到问题了!这是 JsonUtility,我没有仔细阅读它的文档。首先,创建并初始化一个新的 SavableDataShell 对象至关重要。然后我必须使用 JsonUtility.FromJsonOverwrite 而不是 JsonUtility.FromJson 来覆盖现有对象。因此,我的 save/load 系统的正确 Load() 方法如下:

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;
        }
    }

抱歉,感谢您的宝贵时间!