【unity实战】unity3D中的PRG库存系统和换装系统(附项目源码)

简介: 【unity实战】unity3D中的PRG库存系统和换装系统(附项目源码)

先来看看最终效果

前言

之前2d的换装和库存系统我们都做过不少了,这次就来学习一个3d版本的,其实逻辑和思维都是共通的,但是也会有些细节不同,毕竟3d多了一个轴,废话少说,我们一起开始吧!

素材

https://assetstore.unity.com/packages/2d/gui/fantasy-wooden-gui-free-103811

简单绘制库存UI

前往mixamo获取人物模型动画

mixamo网站我之前也推荐过:免费获取游戏素材、工具、国内宝藏游戏博主分享

地址:https://www.mixamo.com/

下载自己喜欢的人物动作模型

拖入角色

获取一些自己喜欢的装备物品模型

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

创建生命药水物品,这里我就用苹果和饮料代替,配置对应的恢复生命值

加入库存

运行效果

最终效果

源码

https://gitcode.net/unity1/unity3d-3dinventorychangeequip

目录
相关文章
|
5月前
|
开发者 图形学 Java
揭秘Unity物理引擎核心技术:从刚体动力学到关节连接,全方位教你如何在虚拟世界中重现真实物理现象——含实战代码示例与详细解析
【8月更文挑战第31天】Unity物理引擎对于游戏开发至关重要,它能够模拟真实的物理效果,如刚体运动、碰撞检测及关节连接等。通过Rigidbody和Collider组件,开发者可以轻松实现物体间的互动与碰撞。本文通过具体代码示例介绍了如何使用Unity物理引擎实现物体运动、施加力、使用关节连接以及模拟弹簧效果等功能,帮助开发者提升游戏的真实感与沉浸感。
169 1
|
4月前
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
413 0
|
4月前
|
图形学 开发者 UED
Unity游戏开发必备技巧:深度解析事件系统运用之道,从生命周期回调到自定义事件,打造高效逻辑与流畅交互的全方位指南
【8月更文挑战第31天】在游戏开发中,事件系统是连接游戏逻辑与用户交互的关键。Unity提供了多种机制处理事件,如MonoBehaviour生命周期回调、事件系统组件及自定义事件。本文介绍如何有效利用这些机制,包括创建自定义事件和使用Unity内置事件系统提升游戏体验。通过合理安排代码执行时机,如在Awake、Start等方法中初始化组件,以及使用委托和事件处理复杂逻辑,可以使游戏更加高效且逻辑清晰。掌握这些技巧有助于开发者更好地应对游戏开发挑战。
184 0
|
5月前
|
开发者 图形学 API
从零起步,深度揭秘:运用Unity引擎及网络编程技术,一步步搭建属于你的实时多人在线对战游戏平台——详尽指南与实战代码解析,带你轻松掌握网络化游戏开发的核心要领与最佳实践路径
【8月更文挑战第31天】构建实时多人对战平台是技术与创意的结合。本文使用成熟的Unity游戏开发引擎,从零开始指导读者搭建简单的实时对战平台。内容涵盖网络架构设计、Unity网络API应用及客户端与服务器通信。首先,创建新项目并选择适合多人游戏的模板,使用推荐的网络传输层。接着,定义基本玩法,如2D多人射击游戏,创建角色预制件并添加Rigidbody2D组件。然后,引入网络身份组件以同步对象状态。通过示例代码展示玩家控制逻辑,包括移动和发射子弹功能。最后,设置服务器端逻辑,处理客户端连接和断开。本文帮助读者掌握构建Unity多人对战平台的核心知识,为进一步开发打下基础。
175 0
|
5月前
|
开发者 图形学 C#
揭秘游戏沉浸感的秘密武器:深度解析Unity中的音频设计技巧,从背景音乐到动态音效,全面提升你的游戏氛围艺术——附实战代码示例与应用场景指导
【8月更文挑战第31天】音频设计在游戏开发中至关重要,不仅能增强沉浸感,还能传递信息,构建氛围。Unity作为跨平台游戏引擎,提供了丰富的音频处理功能,助力开发者轻松实现复杂音效。本文将探讨如何利用Unity的音频设计提升游戏氛围,并通过具体示例代码展示实现过程。例如,在恐怖游戏中,阴森的背景音乐和突然的脚步声能增加紧张感;在休闲游戏中,轻快的旋律则让玩家感到愉悦。
153 0
|
5月前
|
图形学 C# 开发者
Unity粒子系统全解析:从基础设置到高级编程技巧,教你轻松玩转绚丽多彩的视觉特效,打造震撼游戏画面的终极指南
【8月更文挑战第31天】粒子系统是Unity引擎的强大功能,可创建动态视觉效果,如火焰、爆炸等。本文介绍如何在Unity中使用粒子系统,并提供示例代码。首先创建粒子系统,然后调整Emission、Shape、Color over Lifetime等模块参数,实现所需效果。此外,还可通过C#脚本实现更复杂的粒子效果,增强游戏视觉冲击力和沉浸感。
355 0
|
5月前
|
开发者 图形学 前端开发
绝招放送:彻底解锁Unity UI系统奥秘,五大步骤教你如何缔造令人惊叹的沉浸式游戏体验,从Canvas到动画,一步一个脚印走向大师级UI设计
【8月更文挑战第31天】随着游戏开发技术的进步,UI成为提升游戏体验的关键。本文探讨如何利用Unity的UI系统创建美观且功能丰富的界面,包括Canvas、UI元素及Event System的使用,并通过具体示例代码展示按钮点击事件及淡入淡出动画的实现过程,助力开发者打造沉浸式的游戏体验。
145 0
|
5月前
|
图形学 C#
超实用!深度解析Unity引擎,手把手教你从零开始构建精美的2D平面冒险游戏,涵盖资源导入、角色控制与动画、碰撞检测等核心技巧,打造沉浸式游戏体验完全指南
【8月更文挑战第31天】本文是 Unity 2D 游戏开发的全面指南,手把手教你从零开始构建精美的平面冒险游戏。首先,通过 Unity Hub 创建 2D 项目并导入游戏资源。接着,编写 `PlayerController` 脚本来实现角色移动,并添加动画以增强视觉效果。最后,通过 Collider 2D 组件实现碰撞检测等游戏机制。每一步均展示 Unity 在 2D 游戏开发中的强大功能。
271 6
|
5月前
|
图形学 缓存 算法
掌握这五大绝招,让您的Unity游戏瞬间加载完毕,从此告别漫长等待,大幅提升玩家首次体验的满意度与留存率!
【8月更文挑战第31天】游戏的加载时间是影响玩家初次体验的关键因素,特别是在移动设备上。本文介绍了几种常见的Unity游戏加载优化方法,包括资源的预加载与异步加载、使用AssetBundles管理动态资源、纹理和模型优化、合理利用缓存系统以及脚本优化。通过具体示例代码展示了如何实现异步加载场景,并提出了针对不同资源的优化策略。综合运用这些技术可以显著缩短加载时间,提升玩家满意度。
396 5
|
4月前
|
测试技术 C# 图形学
掌握Unity调试与测试的终极指南:从内置调试工具到自动化测试框架,全方位保障游戏品质不踩坑,打造流畅游戏体验的必备技能大揭秘!
【9月更文挑战第1天】在开发游戏时,Unity 引擎让创意变为现实。但软件开发中难免遇到 Bug,若不解决,将严重影响用户体验。调试与测试成为确保游戏质量的最后一道防线。本文介绍如何利用 Unity 的调试工具高效排查问题,并通过 Profiler 分析性能瓶颈。此外,Unity Test Framework 支持自动化测试,提高开发效率。结合单元测试与集成测试,确保游戏逻辑正确无误。对于在线游戏,还需进行压力测试以验证服务器稳定性。总之,调试与测试贯穿游戏开发全流程,确保最终作品既好玩又稳定。
208 4