先来看看最终效果
前言
之前2d的换装和库存系统我们都做过不少了,这次就来学习一个3d版本的,其实逻辑和思维都是共通的,但是也会有些细节不同,毕竟3d多了一个轴,废话少说,我们一起开始吧!
素材
https://assetstore.unity.com/packages/2d/gui/fantasy-wooden-gui-free-103811
简单绘制库存UI
前往mixamo获取人物模型动画
mixamo网站我之前也推荐过:免费获取游戏素材、工具、国内宝藏游戏博主分享
下载自己喜欢的人物动作模型
拖入角色
获取一些自己喜欢的装备物品模型
https://sketchfab.com/Trueform/collections/downloadable-8e49931974d24a8f9b5f77d94328540b
导入模型的材质可能丢失
手动创建一个材质
配置对应纹理
挂载材质
同样的方法,配置其他不同类型的装备物品
库存系统
新增脚本InventoryItem
[CreateAssetMenu(menuName = "ScriptableObjects/库存系统/物品")] public class InventoryItem : ScriptableObject { [SerializeField] private GameObject itemPrefab; // 存储物品的预制体 [SerializeField] private Sprite itemSprite; // 存储物品的精灵 [SerializeField] private string itemName; // 存储物品的名称 [SerializeField] private Vector3 itemLocalPosition; // 存储物品的局部位置 [SerializeField] private Vector3 itemLocalRotation; // 存储物品的局部旋转 // 返回存储的物品精灵 public Sprite GetSprite() { return itemSprite; } // 返回存储的物品名称 public string GetName() { return itemName; } // 返回存储的物品预制体 public GameObject GetPrefab() { return itemPrefab; } // 返回存储的物品局部位置 public Vector3 GetLocalPosition() { return itemLocalPosition; } // 返回存储的物品局部旋转(以四元数表示) public Quaternion GetLocalRotation() { return Quaternion.Euler(itemLocalRotation); } }
配置不同物品信息
新增InventoryItemWrapper
// 使用[System.Serializable]属性将该类标记为可序列化,以便在Unity编辑器中进行序列化 [System.Serializable] public class InventoryItemWrapper { [SerializeField] private InventoryItem item; // 存储物品信息的对象 [SerializeField] private int count; // 存储物品数量 // 返回存储的物品信息 public InventoryItem GetItem() { return item; } // 返回存储的物品数量 public int GetItemCount() { return count; } }
新增Inventory
[CreateAssetMenu(menuName = "ScriptableObjects/库存系统/库存")] public class Inventory : ScriptableObject { [SerializeField] private List<InventoryItemWrapper> items = new List<InventoryItemWrapper>(); // 存储物品及其数量的列表 [SerializeField] private InventoryUI inventoryUIPrefab; private InventoryUI _inventoryUI; // 与此库存相关联的UI private InventoryUI inventoryUI { get { if (!_inventoryUI) { _inventoryUI = Instantiate(inventoryUIPrefab, playerEquipment.GetUIParent()); } return _inventoryUI; } } private Dictionary<InventoryItem, int> itemToCountMap = new Dictionary<InventoryItem, int>(); // 将物品映射到数量的字典 private PlayerEquipmentController playerEquipment; // 初始化库存,将物品及其数量添加到映射中 public void InitInventory(PlayerEquipmentController playerEquipment) { this.playerEquipment = playerEquipment; for (int i = 0; i < items.Count; i++) { itemToCountMap.Add(items[i].GetItem(), items[i].GetItemCount()); } } //开启背包 public void OpenInventoryUI() { inventoryUI.gameObject.SetActive(true); inventoryUI.InitInventoryUI(this); } // 分配物品给玩家 public void AssignItem(InventoryItem item) { Debug.Log("点击了物品:" + item.GetName()); } // 返回所有物品及其数量的映射 public Dictionary<InventoryItem, int> GetAllItemsMap() { return itemToCountMap; } // 添加物品到库存中,并更新UI public void AddItem(InventoryItem item, int count) { int currentItemCount; if (itemToCountMap.TryGetValue(item, out currentItemCount)) { itemToCountMap[item] = currentItemCount + count; } else { itemToCountMap.Add(item, count); } inventoryUI.CreateOrUpdateSlot(this, item, count); } // 从库存中移除物品,并更新UI public void RemoveItem(InventoryItem item, int count) { int currentItemCount; if (itemToCountMap.TryGetValue(item, out currentItemCount)) { itemToCountMap[item] = currentItemCount - count; if (currentItemCount - count <= 0) { inventoryUI.DestroySlot(item); } else { inventoryUI.UpdateSlot(item, currentItemCount - count); } } else { Debug.Log("Can't remove item"); } } }
配置库存信息
新增InventorySlot,控制物品插槽信息显示
public class InventorySlot : MonoBehaviour { [SerializeField] private Image itemImage; // 物品图像 [SerializeField] private TextMeshProUGUI itemNameText; // 物品名称文本 [SerializeField] private TextMeshProUGUI itemCountText; // 物品数量文本 [SerializeField] private Button slotButton; // 插槽按钮 // 初始化插槽的可视化表示 public void InitSlotVisualisation(Sprite itemSprite, string itemName, int itemCount) { itemImage.sprite = itemSprite; itemNameText.text = itemName; UpdateSlotCount(itemCount); } // 更新插槽中物品的数量显示 public void UpdateSlotCount(int itemCount) { itemCountText.text = itemCount.ToString(); } // 分配插槽按钮的回调函数 public void AssignSlotButtonCallback(System.Action onClickCallback) { slotButton.onClick.AddListener(() => onClickCallback()); } }
挂载脚本并配置信息
新增InventoryUI,控制显示背包插槽信息
public class InventoryUI : MonoBehaviour { [SerializeField] private Transform slotsParent; // 插槽的父级对象 [SerializeField] private InventorySlot slotPrefab; // 插槽的预制体 private Dictionary<InventoryItem, InventorySlot> itemToSlotMap = new Dictionary<InventoryItem, InventorySlot>(); // 将物品映射到插槽的字典 // 初始化库存UI public void InitInventoryUI(Inventory inventory) { var itemsMap = inventory.GetAllItemsMap(); foreach (var kvp in itemsMap) { CreateOrUpdateSlot(inventory, kvp.Key, kvp.Value); } } // 创建或更新物品插槽 public void CreateOrUpdateSlot(Inventory inventory, InventoryItem item, int itemCount) { if (!itemToSlotMap.ContainsKey(item)) { var slot = CreateSlot(inventory, item, itemCount); itemToSlotMap.Add(item, slot); } else { UpdateSlot(item, itemCount); } } // 更新已存在的物品插槽 public void UpdateSlot(InventoryItem item, int itemCount) { itemToSlotMap[item].UpdateSlotCount(itemCount); } // 创建物品插槽 private InventorySlot CreateSlot(Inventory inventory, InventoryItem item, int itemCount) { var slot = Instantiate(slotPrefab, slotsParent); slot.InitSlotVisualisation(item.GetSprite(), item.GetName(), itemCount); slot.AssignSlotButtonCallback(() => inventory.AssignItem(item)); return slot; } // 销毁物品插槽 public void DestroySlot(InventoryItem item) { Destroy(itemToSlotMap[item].gameObject); itemToSlotMap.Remove(item); } }
挂载脚本配置信息
新增PlayerEquipmentController,初始化库存
public class PlayerEquipmentController : MonoBehaviour { [SerializeField] private Inventory inventory; // 玩家的库存 [SerializeField] private Transform inventoryUIParent; // 库存UI的父级对象 private void Start() { inventory.InitInventory(this); // 初始化玩家库存 inventory.OpenInventoryUI(); // 打开库存UI } // 获取UI父级对象 public Transform GetUIParent() { return inventoryUIParent; } }
挂载脚本,并配置信息
效果
换装系统
修改InventoryItem,将InventoryItem 定义为所有物品的抽象父类,AssignItemToPlayer方法声明为抽象方法。这意味着所有继承自InventoryItem的子类都必须实现这个方法。这样可以确保每个具体的物品类在被分配给玩家时都有自己特定的行为
public abstract class InventoryItem : ScriptableObject { //。。。 //将物品分配给玩家 public abstract void AssignItemToPlayer(PlayerEquipmentController playerEquipment); }
修改Inventory,调用AssignItemToPlayer方法
// 分配物品给玩家 public void AssignItem(InventoryItem item) { // Debug.Log("点击了物品:" + item.GetName()); //将物品分配给玩家 item.AssignItemToPlayer(playerEquipment); }
新增HelmetInventoryItem,定义头盔物品类
[CreateAssetMenu(menuName = "ScriptableObjects/库存系统/物品/头盔")] public class HelmetInventoryItem : InventoryItem { // 将物品分配给玩家 public override void AssignItemToPlayer(PlayerEquipmentController playerEquipment) { playerEquipment.AssignHelmetItem(this); } }
新增HandInventoryItem,定义手部物品类
public enum Hand { LEFT, // 左手 RIGHT // 右手 } [CreateAssetMenu(menuName = "ScriptableObjects/库存系统/物品/手部物品")] public class HandInventoryItem : InventoryItem { public Hand hand; // 物品所属的手部类型,左手或右手 // 将物品分配给玩家 public override void AssignItemToPlayer(PlayerEquipmentController playerEquipment) { playerEquipment.AssignHandItem(this); } }
新增ArmorInventoryItem,定义护甲物品类
[CreateAssetMenu(menuName = "ScriptableObjects/库存系统/物品/护甲")] public class ArmorInventoryItem : InventoryItem { // 将物品分配给玩家 public override void AssignItemToPlayer(PlayerEquipmentController playerEquipment) { playerEquipment.AssignArmorItem(this); } }
修改PlayerEquipmentController,定义不同部位物品数据处理逻辑
[SerializeField] private Transform helmetAnchor; // 头盔装备点 [SerializeField] private Transform leftAnchor; // 左手装备点 [SerializeField] private Transform rightAnchor; // 右手装备点 [SerializeField] private Transform armorAnchor; // 盔甲装备点 private GameObject currentHelmetObj; // 当前头盔对象 private GameObject currentLeftHandObj; // 当前左手对象 private GameObject currentRightHandObj; // 当前右手对象 private GameObject currentArmorObj; // 当前盔甲对象 // 分配头盔物品给玩家 public void AssignHelmetItem(HelmetInventoryItem item) { DestroyIfNotNull(currentHelmetObj); // 如果当前有头盔对象,则销毁 currentHelmetObj = CreateNewItemInstance(item, helmetAnchor); // 创建新的头盔实例并赋值给当前头盔对象 } // 创建新的装备实例 private GameObject CreateNewItemInstance(InventoryItem item, Transform anchor) { var itemInstance = Instantiate(item.GetPrefab(), anchor); // 实例化物品的预制体,并放置在指定的装备点 itemInstance.transform.localPosition = item.GetLocalPosition(); // 设置物品相对于装备点的本地坐标 itemInstance.transform.localRotation = item.GetLocalRotation(); // 设置物品相对于装备点的本地旋转 return itemInstance; // 返回创建的物品实例 } // 销毁物体,如果不为空 private void DestroyIfNotNull(GameObject obj) { if (obj != null) { Destroy(obj); } } // 分配手部物品给玩家 public void AssignHandItem(HandInventoryItem item) { switch (item.hand) { case Hand.LEFT: DestroyIfNotNull(currentLeftHandObj); currentLeftHandObj = CreateNewItemInstance(item, leftAnchor); break; case Hand.RIGHT: DestroyIfNotNull(currentRightHandObj); currentRightHandObj = CreateNewItemInstance(item, rightAnchor); break; default: break; } } // 分配盔甲物品给玩家 public void AssignArmorItem(ArmorInventoryItem item) { DestroyIfNotNull(currentArmorObj); // 如果当前有盔甲对象,则销毁 currentArmorObj = CreateNewItemInstance(item, armorAnchor); // 创建新的盔甲实例并赋值给当前盔甲对象 }
配置
添加新的库存物品配置,删除旧的
运行效果
装备偏移问题
可以看到装备物品存在偏移,运行修改装备到合适位置,复制装备位置和旋转进对应装备的偏移参数
效果
添加消耗品
新增HealthPotionInventoryItem,定义生命药水物品类
[CreateAssetMenu(menuName = "ScriptableObjects/库存系统/物品/生命药水")] public class HealthPotionInventoryItem : InventoryItem { [SerializeField] private int healthPoints; // 生命药水的恢复生命值 public override void AssignItemToPlayer(PlayerEquipmentController playerEquipment) { playerEquipment.AssingHealthPotionItem(this); } public int GetHealthPoints() // 获取生命药水的恢复生命值 { return healthPoints; } }
修改PlayerEquipmentController
private int playerHealth = 0; // 分配生命药水物品给玩家 public void AssingHealthPotionItem(HealthPotionInventoryItem item) { inventory.RemoveItem(item, 1);// 消耗物品 playerHealth += item.GetHealthPoints();//加血 Debug.Log("玩家现在生命值" + playerHealth); }
创建生命药水物品,这里我就用苹果和饮料代替,配置对应的恢复生命值
加入库存
运行效果
最终效果