Unity2D初级背包设计中篇 MVC分层撰写(万字详解)

本人能力有限,如有不足还请斧正,理论分析链接如下:

Unity2D初级背包设计前篇 理论分析-CSDN博客

目录

1.M层:数据存储

物品

仓库容器

加载方式

2.M层:逻辑撰写

[InventoryManager 仓库的管理](#InventoryManager 仓库的管理)

[SlotData 物品的增删查改*(逻辑层)](#SlotData 物品的增删查改*(逻辑层))

3.V层:搭建UI

[1.SlotUI 将物品绘制到格子上](#1.SlotUI 将物品绘制到格子上)

2.三个类

BarUI工具栏的选中与数字切换

BarSoltUI格子高亮

[BagUI 将格子数据绘制到界面上](#BagUI 将格子数据绘制到界面上)

4.C层:玩家控制类

[ItemMoveHandler 物品的增删查改*(表现层)](#ItemMoveHandler 物品的增删查改*(表现层))

[Plyaer 拾取与丢弃](#Plyaer 拾取与丢弃)


1.M层:数据存储

物品

一个2D的物品必不可少的就是类型,图标,物体(这个指Gameobjcet和其名字)堆叠数量

拓展思路:稀有度,价值,耐久,描述,重量,

cs 复制代码
using UnityEngine;
/// <summary>
/// 不同的物品可以有不同的类型 以用作其他逻辑的判断
/// 举例:食物可以吃但通常不能打人
///       武器通常不能吃但可以打人
/// </summary>
public enum ItemType{
    a,
    b,
    c,
    d
}
/// <summary>
///
/// </summary>
[CreateAssetMenu()]
public class ItemData:ScriptableObject
{
    public ItemType itmeType;
    public Sprite itemSprite;
    public GameObject itemPrefab;
    public int maxCount =1;//最大堆叠数量:默认:1
}

另外可能需要一个外部类去定义其属性,比如可拾取的,不可拾取的 这个的作用体现在玩家拾取方面,需要提前知道一下

cs 复制代码
public class Pickable : MonoBehaviour
{
    public ItemType thisItemType;
}

so对象举例

仓库容器

先认识一个单词:Inventory,仓库,库存 在本文中特指背包类

之所以要对Inventory写为So,是因为游戏中可能存在许多的仓库,在玩家手中叫做背包,物品栏,在NPC手中就可能叫做商店,锻造了,因此:

cs 复制代码
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu()]
public class InventoryData :ScriptableObject{
    //TODO :拓展 仓库名称,最大容量,自动扩容

    public List<SlotData> slotList;
}

创建两个

可以看到其容量是我们自定义的,也就是写死的没有自动扩容功能

因此在今后的拓展之中可以优化

加载方式

2.M层:逻辑撰写

InventoryManager 仓库的管理

我们需要一个仓库管理器去管理所有的背包所以用到了InventoryData 对象

另外,管理所有物品的时候也应需要一个容器去预加载,所以用到了字典

可以先不看这一行:我们需要为外部提供提供向背包添加物品的方法,所以用到了单例模式

cs 复制代码
using System.Collections.Generic;
using UnityEngine;

public class InventoryManager : MonoBehaviour {
    private static InventoryManager instance;
    public static InventoryManager Instance => instance;

    public InventoryData BagInventory;
    public InventoryData ToolBarInventory;

    public Dictionary<ItemType, ItemData> itemDataDict = new Dictionary<ItemType, ItemData>();

    private void Awake() {
        if (instance == null) {
            instance = this;
        }
        else {
            Destroy(this);
        }
        InitInventoryData();
    }

    private void Start() {

    }

    /// <summary>
    /// 加载本地物品和已有的背包数据
    /// </summary>
    private void InitInventoryData() {
        ItemData[] itemDatas = Resources.LoadAll<ItemData>("Itmes");

        foreach (var singleItem in itemDatas) {
            itemDataDict.Add(singleItem.itmeType, singleItem);
        }

        BagInventory = Resources.Load<InventoryData>("Inventorys/MyInventory");
        ToolBarInventory= Resources.Load<InventoryData>("Inventorys/ToolBarInventory");
    }

    /// <summary>
    /// 通过指定的物品类型从字典中获取物品数据
    /// </summary>
    /// <param name="type">物品类型</param>
    /// <returns>如果找到则返回物品数据,否则返回null</returns>
    private ItemData GetItem(ItemType type) {
 
        if (itemDataDict.TryGetValue(type, out var item))
            return item;
        else {
            Debug.LogError("未找到指定物品");
            return null;
        }
    }

    /// <summary>
    /// 向背包中添加物品
    /// </summary>
    /// <param name="itemType">要添加的物品类型</param>
    public void AddItemToInventory(ItemType itemType) {
        ItemData aItem = GetItem(itemType);

        // 情况1:字典中有该物品且格子未满 如果格子满了将会走情况2
        foreach (var slot in BagInventory.slotList) {
            if (slot.item == aItem && slot.CanAddItem()) {
                slot.Add();
                return;
            }
        }

        // 情况2:格子为空,添加物品
        foreach (var slot in BagInventory.slotList) {
            if (slot.count == 0) {
                slot.AddItem(aItem);
                return;
            }
        }

        // 情况3:背包已满
        Debug.LogWarning($"未能成功添加物品 {aItem.name} 因为 {BagInventory.name} 已经满了");
    }
}

SlotData 物品的增删查改*(逻辑层)

这个类将目光聚焦于背包中的格子,因为它才是背包的最小单位

格子需要持有物品类,并且有一个当前数量的变量

每次修改格子物品信息的时候需要给到其通知(也就是M层----->V层这一步),所以需要做发布者发布一个委托,让V层去监听

cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class SlotData {
    public ItemData item;
    public int currentCount = 0; // 物品数量

    private Action OnChange;

    #region 增(Add)

    // 添加物品到槽位
    public void Add(int numToAdd = 1) {
        this.currentCount += numToAdd;
        OnChange?.Invoke();
    }

    // 设置槽位的物品和数量
    public void AddItem(ItemData item, int count = 1) {
        this.item = item;
        this.currentCount = count;
        OnChange?.Invoke();
    }

    #endregion

    #region 删(Remove)

    // 减少槽位中的物品数量
    public void Reduce(int numToReduce = 1) {
        currentCount -= numToReduce;
        if (currentCount == 0) {
            Clear();
        }
        else {
            OnChange?.Invoke();
        }
    }

    // 清空槽位
    public void Clear() {
        item = null;
        currentCount = 0;
        OnChange?.Invoke();
    }

    #endregion

    #region 查(Check)

    // 检查槽位是否为空
    public bool IsEmpty() {
        return currentCount == 0;
    }

    // 检查槽位是否可以添加物品
    public bool CanAddItem() {
        return currentCount < item.maxCount;
    }

    // 获取槽位的空余空间
    public int GetFreeSpace() {
        return item.maxCount - currentCount;
    }

    #endregion

    #region 改(Update)

    // 移动槽位数据
    public void MoveSlot(SlotData data) {
        this.item = data.item;
        this.currentCount = data.currentCount;
        OnChange?.Invoke();
    }

    // 添加监听器
    public void AddListener(Action OnChange) {
        this.OnChange = OnChange;
    }

    #endregion
}

就这么点东西,至此M层就算是写完辣

3.V层:搭建UI

1.SlotUI 将物品绘制到格子上

自然其应持有SlotData类,并订阅其发布的委托,回调函数就是ChangeUI方法

SetData实现的就是C---->M层,GetData是V---->C层

cs 复制代码
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class SlotUI : MonoBehaviour,IPointerClickHandler
{
    protected SlotData slotData;

    protected Image icon;
    protected TextMeshProUGUI num;
    

    private void Start() {
        icon = transform.Find("icon").GetComponent<Image>();
        num = transform.Find("num").GetComponent<TextMeshProUGUI>();
    
    }
    public SlotData GetData(){ 
        return slotData;
    }

    /// <summary>
    /// 为该脚本上的对象赋值一个SlotData
    /// </summary>
    public void SetData(SlotData slotData) { 
        this.slotData = slotData;

        //事件监听 - 订阅者
        slotData.AddListener(UpdateUI2Slot);

        UpdateUI2Slot();
    }

    / <summary>
    / 监听对象
    / </summary>
    //public void ChangeUI(){
    //    UpdateUI2Slot();
    //}

    private void UpdateUI2Slot(){

        if (slotData==null || slotData.item == null || slotData.currentCount <= 0) {
           
            icon.enabled = false;
            num.enabled = false;
        }
        else {
            icon.enabled = true;
            num.enabled = true;
            icon.sprite = slotData.item.itemSprite;
            num.text = slotData.currentCount.ToString();

        }
    }

    public void OnPointerClick(PointerEventData eventData) {
        Debug.Log("发生了点击");
        ItemMoveHandler.Instance.OnSlotClick(this);
    }
}

选中后将物品依附到鼠标上面

2.三个类

BarUI工具栏的选中与数字切换

cs 复制代码
using System.Collections.Generic;
using UnityEngine;

public class BarUI : MonoBehaviour
{
    //工具栏ui列表 先给这个列表对应好ui格子,之后将数据列表粘贴到ui列表中 并更新UI即完成可视化
    [SerializeField]private List<BarSlotUI> barSlotUIList;

    [SerializeField] private GameObject ContentList;

    [SerializeField] private BarSlotUI curSelectBarSlotUI;//当前选中的工具栏的格子

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        barSlotUIList = new List<BarSlotUI>();
        ContentList =transform.Find("Bar/ContentList").gameObject;
        InitSlotUI();
    }

    private void Update() {
        SelectBarSlot();
    }
    /// <summary>
    /// 默认curSelectBarSlotUI为空,所以首次不会进入第二个if
    /// 当第二次选中格子时,curSelectBarSlotUI指向第一个格子,所以不为空就高亮第一个格子
    /// </summary>
    public void SelectBarSlot(){

        for (int i = (int)KeyCode.Alpha1; i < (int)KeyCode.Alpha9 + 1; i++) {
            if (Input.GetKeyDown((KeyCode)i)) {
                if (curSelectBarSlotUI != null) {
                    curSelectBarSlotUI.BarSlotLight();
                }

                int index = i - (int)KeyCode.Alpha1;
                curSelectBarSlotUI = barSlotUIList[index];
                curSelectBarSlotUI.BarSlotDark();

            }
        }
    }
    public void InitSlotUI() {
        if (ContentList != null) {
            foreach (BarSlotUI barSlotUI in ContentList.GetComponentsInChildren<SlotUI>()) {
                barSlotUIList.Add(barSlotUI);
            }
        }
        UpdataUI();
    }
    public void UpdataUI() {
        for (int i = 0; i < InventoryManager.Instance.ToolBarInventory.slotList.Count; i++) {
            barSlotUIList[i].SetData(InventoryManager.Instance.ToolBarInventory.slotList[i]);
        }
    }


}

BarSoltUI格子高亮

cs 复制代码
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class BarSlotUI : SlotUI
{
    [SerializeField] private Sprite slotLight;
    [SerializeField] private Sprite slotDark;
    [SerializeField]private Image thisImage;
    
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        InitBar_Slot();
    }

    public void InitBar_Slot(){
        this.icon = transform.Find("Bar_icon").GetComponent<Image>();
        
        this.num = transform.Find("Bar_num").GetComponent<TextMeshProUGUI>();

        thisImage =GetComponent<Image>();

        slotLight = Resources.Load<Sprite>("SlotUI/slotLight");
        slotDark = Resources.Load<Sprite>("SlotUI/slotDark");
    }

    public void BarSlotLight(){
        thisImage.sprite = slotLight;
    }

    public void BarSlotDark() {
        thisImage.sprite = slotDark;
    }
}

BagUI 将格子数据绘制到界面上

cs 复制代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class BagUI : MonoBehaviour {
    [SerializeField] private Button close;
    [SerializeField] private GameObject BG;
    [SerializeField] private GameObject slotGrid;
    [SerializeField] private List<SlotUI> soltuiList = new List<SlotUI>();

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start() {
        InitElement();
        InitSlotUI();
    }

    // Update is called once per frame
    void Update() {
        ColseBag();
    }
    public void InitElement() {
        BG = transform.Find("BG").gameObject;

        close = transform.Find("BG/BgElement/Close").GetComponent<Button>();

        slotGrid = transform.Find("BG/SlotGrid").gameObject;
        if (close != null) {
            close.onClick.AddListener(() => {
                if (BG != null)
                    BG.SetActive(!BG.activeSelf);
                else {
                    Debug.LogWarning("没找到BG对象");
                    return;
                }
            });
        }
        else
            Debug.LogWarning("没有加载到close按钮");

    }

    public void UpdataUI() {
        for (int i = 0; i < InventoryManager.Instance.BagInventory.slotList.Count; i++) {
            soltuiList[i].SetData(InventoryManager.Instance.BagInventory.slotList[i]);
        }
    }

    public void InitSlotUI() {
        if (slotGrid != null) {
            foreach (SlotUI slotUi in slotGrid.GetComponentsInChildren<SlotUI>()) {
                soltuiList.Add(slotUi);
            }
        }
        UpdataUI();
    }


    public void ColseBag() {
        if (Input.GetKeyDown(KeyCode.Tab))
            BG.SetActive(!BG.activeSelf);
    }
}

4.C层:玩家控制类

ItemMoveHandler 物品的增删查改*(表现层)

这个类其实应该好好讲一讲,但是笔者写了一个多小时雀氏有点累了,反正都是一些常见的逻辑 所以用ai给到注释,如果前面的代码都理解了,那么这里一点也不难

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class ItemMoveHandler : MonoBehaviour {
    // 单例模式的实例
    public static ItemMoveHandler Instance {
        get; private set;
    }

    // 图标
    private Image icon;
    // 选中的槽数据
    private SlotData selectedSlotData;

    // 玩家对象
    private Player player;

    // 控制键是否按下
    private bool isCtrlDown = false;

    // Awake方法在脚本实例化时调用
    private void Awake() {
        Instance = this;
        icon = GetComponentInChildren<Image>();
        HideIcon();
        player = GameObject.FindAnyObjectByType<Player>();
    }

    // Update方法在每帧调用
    private void Update() {
        // 如果图标启用,更新图标位置
        if (icon.enabled) {
            Vector2 position;
            RectTransformUtility.ScreenPointToLocalPointInRectangle(
                GetComponent<RectTransform>(), Input.mousePosition,
                null,
                out position);
            icon.GetComponent<RectTransform>().anchoredPosition = position;
        }

        // 左键点击时,如果没有点击UI元素,丢弃物品
        if (Input.GetMouseButtonDown(0)) {
            if (EventSystem.current.IsPointerOverGameObject() == false) {
                ThrowItem();
            }
        }

        // 检测Ctrl键按下和松开
        if (Input.GetKeyDown(KeyCode.LeftControl)) {
            isCtrlDown = true;
        }
        if (Input.GetKeyUp(KeyCode.LeftControl)) {
            isCtrlDown = false;
        }
        // 右键点击时,强制清空手上的物品
        if (Input.GetMouseButtonDown(1)) {
            ClearHandForced();
        }
    }

    // 槽点击事件处理
    public void OnSlotClick(SlotUI slotui) {
        // 判断手上是否有物品
        if (selectedSlotData != null) {
            // 手上有物品
            if (slotui.GetData().IsEmpty()) {
                // 当前点击了一个空格子
                MoveToEmptySlot(selectedSlotData, slotui.GetData());
            }
            else {
                // 当前点击了一个非空格子
                if (selectedSlotData == slotui.GetData())
                    return;
                else {
                    // 点击了别的格子 且 两个格子的物品相同
                    if (selectedSlotData.item == slotui.GetData().item) {
                        MoveToNotEmptySlot(selectedSlotData, slotui.GetData());
                    }
                    else {
                        SwitchData(selectedSlotData, slotui.GetData());
                    }
                }
            }
        }
        else {
            // 手上没有物品
            if (slotui.GetData().IsEmpty())
                return;
            selectedSlotData = slotui.GetData();
            ShowIcon(selectedSlotData.item.itemSprite);
        }
    }

    // 隐藏图标
    void HideIcon() {
        icon.enabled = false;
    }

    // 显示图标
    void ShowIcon(Sprite sprite) {
        icon.sprite = sprite;
        icon.enabled = true;
    }

    // 清空手上的物品
    void ClearHand() {
        if (selectedSlotData.IsEmpty()) {
            HideIcon();
            selectedSlotData = null;
        }
    }

    // 强制清空手上的物品
    void ClearHandForced() {
        HideIcon();
        selectedSlotData = null;
    }

    // 丢弃物品
    private void ThrowItem() {
        if (selectedSlotData != null) {
            GameObject prefab = selectedSlotData.item.itemPrefab;
            int count = selectedSlotData.currentCount;
            if (isCtrlDown) {
                player.ThrowItem2Creat(prefab, 1);
                selectedSlotData.Reduce();
            }
            else {
                player.ThrowItem2Creat(prefab, count);
                selectedSlotData.Clear();
            }
            ClearHand();
        }
    }

    // 移动物品到空槽
    private void MoveToEmptySlot(SlotData fromData, SlotData toData) {
        if (isCtrlDown) {
            toData.AddItem(fromData.item);
            fromData.Reduce();
        }
        else {
            toData.MoveSlot(fromData);
            fromData.Clear();
        }
        ClearHand();
    }

    // 移动物品到非空槽
    private void MoveToNotEmptySlot(SlotData fromData, SlotData toData) {
        if (isCtrlDown) {
            if (toData.CanAddItem()) {
                toData.Add();
                fromData.Reduce();
            }
        }
        else {
            int freespace = toData.GetFreeSpace();
            if (fromData.currentCount > freespace) {
                toData.Add(freespace);
                fromData.Reduce(freespace);
            }
            else {
                toData.Add(fromData.currentCount);
                fromData.Clear();
            }
        }
        ClearHand();
    }

    // 交换槽数据
    private void SwitchData(SlotData data1, SlotData data2) {
        ItemData item = data1.item;
        int count = data1.currentCount;
        data1.MoveSlot(data2);
        data2.AddItem(item, count);
        ClearHandForced();
    }
}

Plyaer 拾取与丢弃

cs 复制代码
using UnityEngine;

public class Player : MonoBehaviour
{
   
    private void OnTriggerEnter2D(Collider2D collision) {
        Debug.Log("进入了触发检测");
        if (collision.CompareTag("Item")){
            Debug.Log("检测到了物品");
            InventoryManager.Instance.AddItemToInventory(collision.GetComponent<Pickable>().thisItemType);
            Destroy(collision.gameObject);
        }
    }

    /// <summary>
    /// 丢弃背包的物品 并在场景中实例化出来
    /// </summary>
    /// <param name="itemPrefab">丢弃对象</param>
    /// <param name="count">丢弃数量</param>
    public void ThrowItem2Creat(GameObject itemPrefab,int count){ 
        for(int i = 0;  i < count; i++){
       
            GameObject go  =Instantiate(itemPrefab);
            Vector2 dir = Random.insideUnitCircle.normalized * 0.8f;
            go.transform.position = new Vector3(dir.x,dir.y,0);
        } 
    }
}
相关推荐
军训猫猫头8 小时前
插曲.带“?“的类型
c#
格林威10 小时前
Baumer工业相机堡盟LXT工业相机如何升级固件使得相机具有RDMA功能
人工智能·数码相机·算法·计算机视觉·c#
军训猫猫头12 小时前
34.键盘1 C#例子 WPF例子
开发语言·c#·wpf
sukalot14 小时前
windows C#-事件
开发语言·c#
yutian060614 小时前
C# 去除字符串中首尾、首、尾、的空格
开发语言·c#
yutian060614 小时前
C#: button 防止按钮在短时间内被连续点击的方法
开发语言·c#
龚子亦15 小时前
Unity学习之UGUI(二)
学习·unity·游戏引擎
一水鉴天16 小时前
智能工厂的设计软件 应用场景的一个例子: 为AI聊天工具添加一个知识系统 之21 项目主页:基于资源的交互系统--以RESTful 风格设计构建 聊天窗口
开发语言·c#
忧郁的蛋~16 小时前
C#数据库操作系列---SqlSugar完结篇
数据库·c#·asp.net
Cikiss17 小时前
SpringMVC解析
java·服务器·后端·mvc