【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili
教程源地址:https://www.udemy.com/course/2d-rpg-alexdev/
本章节实现了仓库物品的保存

SerializableDictionary.cs
Unity 中的序列化问题
- 标准字典无法序列化:Unity 的序列化系统(如 Inspector 显示或存档功能)不支持
Dictionary<TKey, TValue>,因为它无法直接保存键值对的关系。 - 解决方法:通过两个列表(
keys和values)分别保存字典的键和值,再通过自定义逻辑将列表和字典互相转换。
序列化前处理 (OnBeforeSerialize)
- 触发时机:当 Unity 序列化对象时(如保存场景或进入播放模式)。
- 作用:
- 清空现有的
keys和values列表。 - 遍历字典中的所有键值对,将键存入
keys列表,值存入values列表。
- 清空现有的
- 结果:字典中的数据被转换为两个可序列化的列表。
反序列化后处理 (OnAfterDeserialize)
- 触发时机:当 Unity 反序列化对象时(如加载场景或从存档中还原数据)。
- 作用:
- 清空字典,确保它是空的。
- 检查
keys和values列表的长度是否一致。如果不一致,打印错误信息。 - 遍历
keys和values,将键值对逐一添加到字典中。
- 结果:列表中的数据被还原到字典中。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;[System.Serializable]
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
{[SerializeField] private List<TKey> keys = new List<TKey>();[SerializeField] private List<TValue> values = new List<TValue>();public void OnBeforeSerialize(){keys.Clear();values.Clear();foreach(KeyValuePair<TKey, TValue> kvp in this){keys.Add(kvp.Key);values.Add(kvp.Value);}}public void OnAfterDeserialize(){this.Clear();if(keys.Count != values.Count){Debug.Log("键数和值数不相等");//} for (int i = 0; i < keys.Count; i++){this.Add(keys[i], values[i]);}}}
Inventory.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;//放在创建仓库的empyty对象上,就是仓库的运行函数
public class Inventory : MonoBehaviour, ISaveManager//实现了ISaveManager接口
{public static Inventory instance;//单例模式public List<ItemData> startingItems;//初始装备//两种关键的存贮结构//List:存储玩家的装备、仓库物品、储藏室物品等//Dictionary:存储每个物品的数据以及物品在仓库中的具体信息,例如物品的堆叠数量public List<InventoryItem> equipment;public Dictionary<ItemData_Equipment, InventoryItem> equipmentDictionary;public List<InventoryItem> inventory;public Dictionary<ItemData, InventoryItem> inventoryDictionary;public List<InventoryItem> stash;public Dictionary<ItemData, InventoryItem> stashDictionary;//UI物品槽管理:通过以下代码将游戏中的物品槽和UI界面上的物品槽关联起来[Header("仓库UI")]//Inventory UI[SerializeField] private Transform inventorySlotParent;//位置[SerializeField] private Transform stashSlotParent;[SerializeField] private Transform equipmentSlotParent;[SerializeField] private Transform statSlotParent;//物品和材料的存贮位置分开private UI_ItemSlot[] inventoryItemSlot;private UI_ItemSlot[] stashItemSlot;//储藏室private UI_EquipmentSlot[] equipmentSlot;//装备private UI_StatSlot[] statSlot;[Header("物品冷却")]private float lastTimeUsedFlask;private float lastTimeUsedArmor;//P122解决一开始不能用物品的问题,因为一开始冷却被赋值,使用了currentFlask.itemCoolDownpublic float flaskCoolDown { get; private set; }private float armorCoolDown;[Header("数据库")]public List<InventoryItem> loadedItems;//加载的物品private void Awake(){if (instance == null)instance = this;elseDestroy(gameObject);//防止从一个地方到另一个地方}private void Start()//初始实例化{inventory = new List<InventoryItem>();inventoryDictionary = new Dictionary<ItemData, InventoryItem>();stash = new List<InventoryItem>();stashDictionary = new Dictionary<ItemData, InventoryItem>();equipment = new List<InventoryItem>();equipmentDictionary = new Dictionary<ItemData_Equipment, InventoryItem>();//同时获取UI中对应的物品槽//获得起始的脚本inventoryItemSlot = inventorySlotParent.GetComponentsInChildren<UI_ItemSlot>();stashItemSlot = stashSlotParent.GetComponentsInChildren<UI_ItemSlot>();equipmentSlot = equipmentSlotParent.GetComponentsInChildren<UI_EquipmentSlot>();statSlot = statSlotParent.GetComponentsInChildren<UI_StatSlot>();AddStartingItems();}private void AddStartingItems()//添加初始物品{if(loadedItems.Count > 0){foreach (InventoryItem item in loadedItems){for (int i = 0; i < item.stackSize; i++){AddItem(item.data);}}return;}for (int i = 0; i < startingItems.Count; i++){if (startingItems[i] != null)AddItem(startingItems[i]);}}public void EquipItem(ItemData _item)//把一个物品装备到角色身上{ItemData_Equipment newEquipment = _item as ItemData_Equipment;//把 _item 对象转换为 ItemData_Equipment 类型,表示它是一个装备物品InventoryItem newItem = new InventoryItem(newEquipment); //把 ItemData 对象转换为 InventoryItem 对象ItemData_Equipment oldEquipment = null;//要删除的物品foreach (KeyValuePair<ItemData_Equipment, InventoryItem> item in equipmentDictionary)//遍历装备字典{if (item.Key.equipmentType == newEquipment.equipmentType)//如果装备类型相同oldEquipment = item.Key;//删除该装备}if (oldEquipment != null){UnequipItem(oldEquipment);AddItem(oldEquipment);//把要删除的物品放回仓库}equipment.Add(newItem);equipmentDictionary.Add(newEquipment, newItem);newEquipment.AddModifiers();//添加装备属性RemoveItem(_item);UpdataSlotsUI();}public void UnequipItem(ItemData_Equipment itemToRemove)//移除装备函数{if (equipmentDictionary.TryGetValue(itemToRemove, out InventoryItem value)){equipment.Remove(value);equipmentDictionary.Remove(itemToRemove);itemToRemove.RemoveModifiers();}}private void UpdataSlotsUI()//更新UI物体的数量{// 更新装备槽for (int i = 0; i < equipmentSlot.Length; i++)//将装备物品槽与一个装备字典中的物品进行匹配,并根据匹配结果更新物品槽的内容{foreach (KeyValuePair<ItemData_Equipment, InventoryItem> item in equipmentDictionary)//遍历装备字典{if (item.Key.equipmentType == equipmentSlot[i].slotType)//这个条件用于确保物品能放入正确的槽位中equipmentSlot[i].UpdataSlot(item.Value);}}// 清空并更新仓库和储藏室的物品槽for (int i = 0; i < inventoryItemSlot.Length; i++)//仓库物品槽{inventoryItemSlot[i].CleanUpSlot();}for (int i = 0; i < stashItemSlot.Length; i++)//储藏室中的物品槽{stashItemSlot[i].CleanUpSlot();}// 重新填充仓库和储藏室for (int i = 0; i < inventory.Count; i++){inventoryItemSlot[i].UpdataSlot(inventory[i]);}for (int i = 0; i < stash.Count; i++){stashItemSlot[i].UpdataSlot(stash[i]);}UpdateStatsUI();}public void UpdateStatsUI(){//更新属性槽for (int i = 0; i < statSlot.Length; i++){statSlot[i].UpdateStatValueUI();}}public void AddItem(ItemData _item)//据物品类型,将物品添加到仓库(inventory)或者储藏室(stash){if (_item.itemType == ItemType.Equipment && CanAddItem()){AddToInventory(_item);}else if (_item.itemType == ItemType.Material){AddToStash(_item);}UpdataSlotsUI();}private void AddToStash(ItemData _item){if (stashDictionary.TryGetValue(_item, out InventoryItem value)){value.AddStack();}else{InventoryItem newItem = new InventoryItem(_item);stash.Add(newItem);stashDictionary.Add(_item, newItem);}}private void AddToInventory(ItemData _item){if (inventoryDictionary.TryGetValue(_item, out InventoryItem value))//字典中检查仓库中是否有这个物品,具体查找的是ItemData,out InventoryItem value如果找,返回与该键相关联的值{value.AddStack();//如果物品已经存在于库存中,则增加其堆叠数量}else{InventoryItem newItem = new InventoryItem(_item);//如果物品不存在,则创建一个新的 InventoryIteminventory.Add(newItem);inventoryDictionary.Add(_item, newItem);}}//检查物品是否已经在仓库里,如果存在则增加堆叠数量,如果不存在则创建新的物品对象并加入仓库public void RemoveItem(ItemData _item){if (inventoryDictionary.TryGetValue(_item, out InventoryItem value)){if (value.stackSize <= 1){inventory.Remove(value);inventoryDictionary.Remove(_item);}else{value.RemoveStack();}}if (stashDictionary.TryGetValue(_item, out InventoryItem stashValue)){if (stashValue.stackSize <= 1)//如果物品的堆叠数量小于等于1,则从库存中删除该物品{stash.Remove(stashValue);stashDictionary.Remove(_item);}else{stashValue.RemoveStack();//否则就减少堆寨数量}}UpdataSlotsUI();}public bool CanAddItem(){if (inventory.Count >= inventoryItemSlot.Length){//Debug.Log("仓库已满");return false;}return true;}public bool CanCraft(ItemData_Equipment _itemToCraft, List<InventoryItem> _requiredMaterials)//判断是否可以合成的函数{List<InventoryItem> materialsToRemove = new List<InventoryItem>();for (int i = 0; i < _requiredMaterials.Count; i++){if (stashDictionary.TryGetValue(_requiredMaterials[i].data, out InventoryItem stashValue))//如果储藏室中没有所需材料{if (stashValue.stackSize < _requiredMaterials[i].stackSize)//数量是否足够{Debug.Log("没有足够的材料");return false;}else{materialsToRemove.Add(stashValue);}}else{Debug.Log("没有足够的材料");return false;}}for (int i = 0; i < materialsToRemove.Count; i++)//使用了就从临时仓库中移除{RemoveItem(materialsToRemove[i].data);}AddItem(_itemToCraft);Debug.Log("这里是你的物品" + _itemToCraft.name);return true;}public List<InventoryItem> GetEquipmentList() => equipment;//获取装备列表public List<InventoryItem> GetStashList() => stash;//获取仓库列表public ItemData_Equipment GetEquipment(EquipmentType _type)//获取装备{ItemData_Equipment equipedItem = null;foreach (KeyValuePair<ItemData_Equipment, InventoryItem> item in equipmentDictionary)//遍历装备字典{if (item.Key.equipmentType == _type)//如果装备类型相同equipedItem = item.Key;//删除该装备}return equipedItem;}public void UseFlask()//使用药水的函数{ItemData_Equipment currentFlask = GetEquipment(EquipmentType.Flask);//获取当前的药水if (currentFlask == null)return;bool canUseFlask = Time.time > lastTimeUsedFlask + flaskCoolDown;//判断是否可以使用药水if (canUseFlask){flaskCoolDown = currentFlask.itemCoolDown;//重置冷却时间,一开始就可以使用物品currentFlask.Effect(null);lastTimeUsedFlask = Time.time;}elseDebug.Log("药水冷却中");}public bool CanUseArmor(){ItemData_Equipment currentArmor = GetEquipment(EquipmentType.Armor);//获取当前装备的护甲信息。if (Time.time > lastTimeUsedArmor + armorCoolDown){//更新冷却时间和使用时间armorCoolDown = currentArmor.itemCoolDown;lastTimeUsedArmor = Time.time;return true;}Debug.Log("护甲正在冷却中");return false;}public void LoadData(GameData _data)//加载数据{foreach(KeyValuePair<string,int> pair in _data.inventory){foreach(var item in GetItemDataBase()){if (item != null && item.itemId == pair.Key){InventoryItem itemToLoad = new InventoryItem(item);itemToLoad.stackSize = pair.Value;loadedItems.Add(itemToLoad);}}}}public void SaveData(ref GameData _data){_data.inventory.Clear();foreach (KeyValuePair<ItemData, InventoryItem> pair in inventoryDictionary){_data.inventory.Add(pair.Key.itemId, pair.Value.stackSize);}}private List<ItemData> GetItemDataBase()//获取物品数据库{List<ItemData> itemDatabase = new List<ItemData>();string[] assetNames = AssetDatabase.FindAssets("", new[] { "Assets/Data/Equipment" });foreach(string SOName in assetNames){var SOpath = AssetDatabase.GUIDToAssetPath(SOName);var itemData = AssetDatabase.LoadAssetAtPath<ItemData>(SOpath);itemDatabase.Add(itemData);}return itemDatabase;}
}
ItemData.cs
using System.Text;
using UnityEditor;
using UnityEngine;
//2024年10月31日
//在敌人图层之上,可以看得到public enum ItemType
{Material,Equipment
}[CreateAssetMenu(fileName = "New Item Data", menuName = "Data/Item")]//在资源管理器中创建新的Item,创建的物品自带这个脚本public class ItemData : ScriptableObject //在编辑器中创建和编辑的类
{public ItemType itemType;//定义你的物品的类型public string itemName;//定义你的物品的名字public Sprite icon;//可以选择图标public string itemId;//物品的ID[Range(0,100)]public float dropChance;//掉落几率protected StringBuilder sb = new StringBuilder();//创建一个字符串生成器private void OnValidate(){
#if UNITY_EDITOR//只在编辑器中运行string path = AssetDatabase.GetAssetPath(this);//获取这个物品的路径itemId = AssetDatabase.AssetPathToGUID(path);//获取这个物品的GUID}
#endifpublic virtual string GetDescription()//虚方法,返回一个字符串{return "";}}
GameData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;//2024.11.25
[System.Serializable]
public class GameData
{public int currency;public SerializableDictionary<string, int> inventory;//物品的名字和数量public GameData(){this.currency = 0;inventory = new SerializableDictionary<string, int>();}}
