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 Singleton 中 Instance
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;
}
}
抱歉,感谢您的宝贵时间!
亲爱的编码社区!
目前我正在使用 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 Singleton 中 Instance
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;
}
}
抱歉,感谢您的宝贵时间!