You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity Engine单例模式未按预期工作/初始化异常求助(自定义存档系统交互引发空引用错误)

Hey Vyacheslav, let's break down the issues causing your NullReferenceException and get your save/load system working properly:


Root Causes of the NullReferenceException

Your crash stems from a combination of serialization limitations, flawed singleton logic, and unsafe delegate handling. Let's go through each problem and fix them step by step:

1. JsonUtility Can't Serialize Your Generic/Dictionary Types

Unity's built-in JsonUtility only supports serializable Unity types, and it does not work with generic classes or Dictionary out of the box. Your GenericDataStorage<T> is a generic class without serialization attributes, so when you serialize SavableDataShell, all storage fields get completely ignored. When you deserialize later, shell.FloatData (and other storage fields) are null, which overwrites the valid storage instances you initialized in Database.Awake()—this is why accessing Database.Instance.FloatData throws a null reference.

2. Unreliable Singleton Implementation

Your singleton's Awake() uses FindObjectsOfType<T>() which can be unpredictable during scene initialization (e.g., it might miss instances that haven't finished Awake yet). A simpler, more reliable approach is to check for an existing instance first.

3. Unsafe Delegate Invocation

Your ContentUpdated delegate is invoked without checking if it's null. While you pass a handler in the Database, any other usage without handlers would crash, and it's a good practice to always guard against null delegates.

4. Invalid Value Initialization in TryLoadValue

value = (T)new object(); will throw errors for value types like float or int—you can't cast a raw object to a value type. Use default(T) instead to get the default value for the type.

5. GameObject Serialization is Impossible

You can't serialize a GameObject reference directly via JSON (it's a scene-specific object with runtime-only data). Instead, save a unique identifier like a prefab path or asset GUID, then load the prefab from Resources/Addressables later.


Fixed Code Implementations

1. Updated Singleton Base Class

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

    protected virtual void Awake() {
        if (Instance == null) {
            Instance = this as T;
            DontDestroyOnLoad(gameObject);
        } else {
            Destroy(gameObject);
        }
    }
}

2. Fixed GenericDataStorage Class

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

[Serializable] // Add this for serialization support (if using Newtonsoft)
public class GenericDataStorage<T> {
    private Dictionary<string, T> _data;
    private Action _contentUpdated;

    public GenericDataStorage(params Action[] contentUpdatedHandlers) {
        _data = new Dictionary<string, T>();
        foreach (Action handler in contentUpdatedHandlers)
            _contentUpdated += handler;
    }

    public bool TryLoadValue(string key, out T value) {
        if (_data.ContainsKey(key)) {
            value = _data[key];
            return true;
        } else {
            value = default(T); // Safe default value instead of invalid cast
            return false;
        }
    }

    public bool TryDeleteValue(string key) {
        if (_data.ContainsKey(key)) {
            _data.Remove(key);
            _contentUpdated?.Invoke(); // Safe null-check before invoking
            return true;
        } else
            return false;
    }

    public void SaveValue(string key, T value) {
        // Handle existing keys to avoid Add() exceptions
        if (_data.ContainsKey(key)) {
            _data[key] = value;
        } else {
            _data.Add(key, value);
        }
        _contentUpdated?.Invoke();
    }
}

3. Switch to Newtonsoft.Json for Serialization

Unity 2019.4 supports Newtonsoft.Json via the Package Manager (search for "Newtonsoft.Json" and install it). This library handles generics and dictionaries seamlessly. Here's the updated Database:

using UnityEngine;
using System.IO;
using System;
using System.Text;
using Newtonsoft.Json;

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; }
    // Replace GameObject storage with string paths/IDs
    public GenericDataStorage<string> PrefabData { get; private set; }
    public GenericDataStorage<string> StringData { get; private set; }

    private void Save() {
        var shell = new SavableDataShell(FloatData, IntData, BooleanData, PrefabData, StringData);
        string databaseAsJson = JsonConvert.SerializeObject(shell, Formatting.Indented);
        byte[] bytesToEncode = Encoding.UTF8.GetBytes(databaseAsJson);
        string encodedText = Convert.ToBase64String(bytesToEncode);
        string filePath = Path.Combine(Application.persistentDataPath, _FILE_NAME);
        File.WriteAllText(filePath, encodedText); // Simpler than StreamWriter
    }

    private void Load() {
        string filePath = Path.Combine(Application.persistentDataPath, _FILE_NAME);
        if (File.Exists(filePath)) {
            string encodedText = File.ReadAllText(filePath);
            byte[] encodedBytes = Convert.FromBase64String(encodedText);
            string decodedText = Encoding.UTF8.GetString(encodedBytes);
            var shell = JsonConvert.DeserializeObject<SavableDataShell>(decodedText);
            
            // Fallback to fresh storage if deserialization fails
            FloatData = shell?.FloatData ?? new GenericDataStorage<float>(Save);
            IntData = shell?.IntData ?? new GenericDataStorage<int>(Save);
            BooleanData = shell?.BooleanData ?? new GenericDataStorage<bool>(Save);
            PrefabData = shell?.PrefabData ?? new GenericDataStorage<string>(Save);
            StringData = shell?.StringData ?? new GenericDataStorage<string>(Save);
        }
    }

    protected override void Awake() {
        base.Awake();
        FloatData = new GenericDataStorage<float>(Save);
        IntData = new GenericDataStorage<int>(Save);
        BooleanData = new GenericDataStorage<bool>(Save);
        PrefabData = new GenericDataStorage<string>(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;
    }
}

4. Updated LevelDifficultyDisplay (with Safety Checks)

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 == null) {
            Debug.LogError("Database instance not found! Ensure exactly one Database exists in your scene.");
            return;
        }

        if (_difficultyDisplay == null) {
            Debug.LogError("Difficulty display Text is not assigned!");
            return;
        }

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

Final Notes

  • Make sure you have exactly one Database GameObject in your initial scene (the singleton will handle DontDestroyOnLoad).
  • For prefab references, save their path in Resources (e.g., "Prefabs/Player") and load them with Resources.Load<GameObject>(path) instead of serializing the GameObject directly.
  • The Newtonsoft.Json package is essential here—Unity's built-in JsonUtility can't handle the generic/dictionary structures you're using.

内容的提问来源于stack exchange,提问作者Heavy Violence

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 09:29:10