Unity开发中存档ItemSlot数组为null且报空引用异常的问题
核心错误定位
直接触发空引用的原因
PlayerData中声明的ItemSlot[] InventorySlots从未初始化,默认值为null,执行数组索引赋值操作时直接抛出NullReferenceException。LoopThroughItemSlots函数逻辑完全失效:foreach内部第一次迭代就执行return slot,后续的break属于永远不会执行的死代码,无论调用多少次都只会返回库存列表的第一个槽位,完全达不到遍历全量槽位的效果。
存档逻辑的底层设计缺陷
ItemSlot是挂载在UI槽位上的MonoBehaviour组件,关联了大量Unity引擎内部引用(GameObject、RectTransform、UI渲染组件等),BinaryFormatter无法正确序列化这类Unity对象,哪怕空引用问题解决,序列化结果也只会是空引用,对应库存数据完全丢失。PlayerData中错误存储了InventoryUI场景组件引用,这类运行时场景对象不属于纯存档数据,序列化时会直接触发异常。- 读档逻辑直接将序列化对象替换
itemSlotList,但itemSlotList存储的是当前场景中已存在的、绑定了UI元素的ItemSlot实例,直接替换会导致UI与数据完全脱节,界面不会更新。 - 存档时使用
FileMode.CreateNew,当存档文件已存在时会直接抛出IOException,无法覆盖写入。
可运行的修复方案
1. 定义纯库存数据类
新建仅存储核心信息的可序列化类,完全避开Unity对象的序列化限制:
[System.Serializable] public class InventorySlotData { // 用全局唯一物品ID代替直接存储物品实例/ScriptableObject public int ItemID; public int StackCount; public InventorySlotData() {} public InventorySlotData(int itemID, int stackCount) { ItemID = itemID; StackCount = stackCount; } }
2. 重构PlayerData类
移除无效的场景组件引用,修正数组初始化逻辑与库存遍历逻辑:
[System.Serializable] public class PlayerData { // 生命值相关变量 public float Health; public float MaxHealth; // 体力相关变量 public float Stamina; public float MaxStamina; public float MovementSpeedWhileSprinting; public float MovementSpeedWhileWalking; public float StaminaDownWhileSprinting; public float StaminaAddWhileSprinting; // 改用纯数据数组存储库存,替换原有的ItemSlot数组 public InventorySlotData[] InventorySlots; public PlayerData(Health health, Sprinting sprinting, InventoryUI invUI) { Health = health.health; MaxHealth = health.maxHealth; Stamina = sprinting.Stamina; MaxStamina = sprinting.MaxStamina; MovementSpeedWhileSprinting = sprinting.MovementSpeedWhileSprinting; MovementSpeedWhileWalking = sprinting.MovementSpeedWhileWalking; StaminaDownWhileSprinting = sprinting.StaminaDownWhileSprinting; StaminaAddWhileSprinting = sprinting.StaminaAddWhileSprinting; SaveInventory(invUI); } private void SaveInventory(InventoryUI invUI) { // 根据当前库存槽位数量初始化数组,解决空引用问题 InventorySlots = new InventorySlotData[invUI.itemSlotList.Count]; for (int i = 0; i < invUI.itemSlotList.Count; i++) { ItemSlot currentSlot = invUI.itemSlotList[i]; // 仅提取核心数据存入纯数据类,不直接存储ItemSlot实例 InventorySlots[i] = new InventorySlotData( currentSlot.CurrentItem.ItemID, currentSlot.StackCount ); } } }
3. 修正SaveAndLoad存档逻辑
替换文件打开模式,避免重复存档时报错:
public static void Save(Health health, Sprinting sprinting, InventoryUI invUI) { BinaryFormatter formatter = new BinaryFormatter(); string path = Application.persistentDataPath + "/player.AutoBox"; // 用FileMode.Create覆盖已有存档,替换原有的FileMode.CreateNew using FileStream stream = new FileStream(path, FileMode.Create); PlayerData data = new PlayerData(health, sprinting, invUI); formatter.Serialize(stream, data); }
4. 修正读档逻辑
不再直接替换itemSlotList,而是将存档数据赋值给当前场景已存在的槽位实例,保证UI正常绑定:
public void Load() { string savePath = Application.persistentDataPath + "/player.AutoBox"; if (!File.Exists(savePath)) return; PlayerData data = SaveAndLoad.Load(); // 生命值、体力相关的赋值逻辑保持不变 health.health = data.Health; health.maxHealth = data.MaxHealth; sprinting.Stamina = data.Stamina; sprinting.MaxStamina = data.MaxStamina; sprinting.MovementSpeedWhileSprinting = data.MovementSpeedWhileSprinting; sprinting.MovementSpeedWhileWalking = data.MovementSpeedWhileWalking; sprinting.StaminaDownWhileSprinting = data.StaminaDownWhileSprinting; sprinting.StaminaAddWhileSprinting = data.StaminaAddWhileSprinting; // 库存读档:遍历当前已存在的UI槽位,写入存档数据 InventoryUI invUI = Inventory.instance.InvUI; for (int i = 0; i < data.InventorySlots.Length; i++) { InventorySlotData slotData = data.InventorySlots[i]; // 通过全局物品ID数据库查询对应物品实例,避免直接序列化物品资产 ItemSO item = ItemDatabase.instance.GetItemByID(slotData.ItemID); invUI.itemSlotList[i].UpdateSlotData(item, slotData.StackCount); } }
冗余代码清理
直接删除逻辑完全错误的LoopThroughItemSlots函数。
内容的提问来源于stack exchange,提问作者user15295683
相关产品推荐
相关产品推荐

