Featured image of post 麦田物语开发日记(十一)

麦田物语开发日记(十一)

完成图纸建造功能及存储箱的储存逻辑

图纸建造功能

图纸的基本数据结构

在该模块中图纸系统,主要通过检测人物背包中的数据内容,然后完成建造功能,因此需要一个基本的So文件来创建构建建造功能的数据存储内容。

然后是显示构造内容的panel,获取该面板中的元素信息,如果鼠标指针检测的物品是可显示内容,因此就显示该Panel,此时Panel的具体内容显示需要通过循环去获取.

构造图纸的基本数据结构

该数据结构用于记录图纸的基本数据信息,以及需要使用的数据的内容。然后在InventoryManager中调用记录的数据结构即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[CreateAssetMenu(fileName = "BluePrintDataList_SO", menuName = "Inventory/BluePrintData")]
public class BluePrintDataList_SO : MonoBehaviour
{
    public List<BluePrintDetails> bluePrintDataList;

    public BluePrintDetails GetBluePrintDetails(int itemId)
    {
        return bluePrintDataList.Find(b => b.ID == itemId);
    }
}

[System.Serializable]
public class BluePrintDetails
{
    public int ID;
    public InventoryItem[] items;
    public int[] itemAmounts;
}

在浮窗中显示图纸所需要的资源信息

获取浮窗对应窗口的控制权,并通过循环的方式,去获得图纸所需要的资源信息和数量,然后以此赋值给浮窗对应的资源内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ItemToolTips.cs
/// <summary>
/// 显示图纸所需要资源的浮窗
/// </summary>
/// <param name="itemID">图纸item的ID</param>
public void SetResourceTooltips(int itemID)
{
    /* 获得图纸蓝图 */
    resourcePanel.gameObject.SetActive(true);
    BluePrintDetails bluePrintDetails = InventoryManager.Instance.bluePrintDataList.GetBluePrintDetails(itemID);
    /* 循环的方式更新UI */
    for (int i = 0; i < resourceImg.Length; i++)
    {
        if (i < bluePrintDetails.items.Length)
        {
            var needResource = bluePrintDetails.items[i];
            var itemDetails = InventoryManager.Instance.GetItemDetails(needResource.itemID);
            if (itemDetails == null) continue;
            resourceImg[i].sprite = itemDetails.itemIcon;
            resourceImg[i].gameObject.SetActive(true);
            resourceImg[i].transform.GetChild(0).GetComponent<TextMeshProUGUI>().text =
                needResource.itemAmount.ToString();
        }
        else
        {
            resourceImg[i].gameObject.SetActive(false);
        }
    }  
}
  • 显示所需资源的具体UI
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// showItemDetails.cs 中的 OnPointerEnter方法
/* 判断当前的物体是图纸 */
if (_slotUI.slotItemDetails.itemType == ItemType.Furniture)
{
    InventoryUI.itemTooltip.SetResourceTooltips(_slotUI.slotItemDetails.itemID);
}
else
{
    InventoryUI.itemTooltip.CloseResourceTooltips();
}

完善建造流程

建造的基本流程:

鼠标能够显示当前物品能否正常建造,因此需要对鼠标的检查函数进行补充

如果可以建造,直接获取图纸的内容,并生成对应的item

  • 对鼠标UI进行改造,增加当前物品是否能建造的图标
image-20230222202700800
  • 当选择内容是图纸的时候,则在鼠标位置显示对应图片
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// CursorManager.cs 的OnItemSelectCursorImage方法
/* 显示建造物品内容  */
if (itemDetail.itemType == ItemType.Furniture)
{
    _buildImg.gameObject.SetActive(true);
    _buildImg.sprite = itemDetail.itemIcon;
    _buildImg.SetNativeSize();
}
else
{
    _buildImg.gameObject.SetActive(false);
}

设置图片跟随鼠标

1
2
/* 设置图片跟随鼠标 */
_buildImg.rectTransform.position = Input.mousePosition;

同时需要注意当与UI互动的时候需要关闭图片

  • 保证鼠标点击的位置能够正确显示可以建造的图片
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// cursorManager.cs 的CheckCursorValid()方法
case ItemType.Furniture:
    _buildImg.gameObject.SetActive(true);
    BluePrintDetails bluePrintDetails = InventoryManager.Instance.bluePrintDataList.GetBluePrintDetails(_currentItem.itemID);
    if (currentTile.canPlaceFurniture && InventoryManager.Instance.CheckStock(bluePrintDetails.ID) &&
        !HaveFurnitureInRadius(bluePrintDetails))
        SetCursorValid();
    else
        SetCursorInvalid();
    break;
  • 检测库存
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// InventoryManager.cs 显示
/// <summary>
/// 检测背包中是否含有满足当前选中的物品
/// </summary>
/// <param name="currentItemID">获得图纸ID</param>
public bool CheckStock(int currentItemID)
{
    BluePrintDetails bluePrintDetails = bluePrintDataList.GetBluePrintDetails(currentItemID);
    foreach (InventoryItem bluePrintItem in bluePrintDetails.items)
    {
        InventoryItem playerBagItem = playerBag.GetPlayerBagItemById(bluePrintItem.itemID);
        if (playerBagItem.itemAmount != 0)
        {
            if (playerBagItem.itemAmount < bluePrintItem.itemAmount)
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }
    return true;
}
  • 检测重叠

使用一个新的脚本Furniture.cs来控制被建造的物品,每个被建造的物品都需要有这个代码

  • 实现构造

GridMapManager.cs中直接呼叫建筑事件,该事件需要ItemManager来控制物体生成,使用InventoryManager来扣除资源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/// <summary>
/// 根据当前蓝图的ID,生成对应的物品
/// </summary>
/// <param name="bluePrintId">蓝图ID</param>
private void OnBuildFurnitureEvent(int bluePrintId, Vector3 mousePos)
{
    BluePrintDetails bluePrintDetails =
        InventoryManager.Instance.bluePrintDataList.GetBluePrintDetails(bluePrintId);
    var BuildItem = Instantiate(bluePrintDetails.buildPrefab, mousePos, Quaternion.identity, _itemParent.transform);
    
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// InventoryManager.cs
/// <summary>
/// 蓝图建造后移除背包资源
/// </summary>
/// <param name="bluePrintId">蓝图ID</param>
/// <param name="mousePos">鼠标的位置</param>
private void OnBuildFurnitureEvent(int bluePrintId, Vector3 mousePos)
{
    BluePrintDetails bluePrintDetails = bluePrintDataList.GetBluePrintDetails(bluePrintId);
    
    /* 移除使用资源 */
    foreach (var bluePrintItem in bluePrintDetails.items)
    {
        RemoveItem(bluePrintItem.itemID, bluePrintItem.itemAmount);
    }
    /* 移除图纸 */
    RemoveItem(bluePrintId, 1);
}

实现存储箱构造,以及相关构造物品的存储。

实现构造物品的存储,和实现场景item的存储方式类似,也是构造建筑物品的信息和对应的坐标,在场景切换前保存对应的构造物品的信息,场景加载时,获得图纸的相关信息并生成对应的物品即可。

  • 基础创建内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// dataCollection.cs中存储的建造物品信息
/// <summary>
/// 场景中由蓝图构造的物品信息
/// </summary>
[System.Serializable]
public class SceneFurniture
{
    // todo 更多信息
    public int bluePrintID;
    public SerializableVector3 itemPos;
}
  • 保证创建的内容在转换场景时也能保存

ItemManager.cs中注册场景切换时候的事件,保证每次移除场景的时候,将当前场景所有物品都存放进对应字典,而加载场景时,从字典中生成所有物品

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ItemManager.cs
/// <summary>
/// 将场景中所有建造物品全部存储在字典中
/// </summary>
private void GetAllSceneFurniture()
{
    List<SceneFurniture> currentFurnitureList = new List<SceneFurniture>();
    /* 获得当前场景所有furniture信息 */
    foreach (Furniture furniture in FindObjectsOfType<Furniture>())
    {
        SceneFurniture newSceneFurniture = new SceneFurniture();
        newSceneFurniture.bluePrintID = furniture.bluePrintId;
        newSceneFurniture.itemPos = new SerializableVector3(furniture.transform.position);
        currentFurnitureList.Add(newSceneFurniture);
    }
    /* 更新当前场景的furniture字典 */
    if (_sceneFurnitureDict.ContainsKey(SceneManager.GetActiveScene().name))
    {
        _sceneFurnitureDict[SceneManager.GetActiveScene().name] = currentFurnitureList;
    }
    else
    {
        _sceneFurnitureDict.Add(SceneManager.GetActiveScene().name, currentFurnitureList);
    }
    
}

/// <summary>
/// 重新构建场景中的所有建造物品
/// </summary>
public void ReBuildSceneFurniture()
{
    List<SceneFurniture> currentFurnitureList = new List<SceneFurniture>();
    /*获取字典中当前场景item的存储*/
    if (_sceneFurnitureDict.TryGetValue(SceneManager.GetActiveScene().name, out currentFurnitureList))
    {
        if (currentFurnitureList != null)
        {
            // 重建所有内容
            foreach (SceneFurniture item in currentFurnitureList)
            {
                OnBuildFurnitureEvent(item.bluePrintID, item.itemPos.ToVector3());
                
            }
        }    
    }
    
}

构造存储箱需要一个专门控制背包脚本,该脚本需要控制背包的具体数据,以及可互动的相关控制,由于会产生多个存储箱,为了避免存储箱的数据一致,因此需要创建一个专门用于保存存储箱数据的SO模板,每个存储箱的数据都是从模板中复制得到的。

  • 制作储物箱的GO

对于储物箱来说,需要有两个碰撞体,一个保证玩家的碰撞,一个设置为触发器,当进入范围并点击对应按键,就可以打开储物箱。

  • 储物箱数据控制

使用InventoryBag_SO构建一个存储箱的数据存储模板BoxBagTemplate,每个储物箱默认数据是从该模板生成的。

然后使用一个专门的代码来保存每个存储箱保有的数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class Box : MonoBehaviour
{
    public InventoryBag_SO bagSOTemplate; /* 数据模板 */
    private InventoryBag_SO _currentBagData; /* 当前箱子保有的数据 */

    private bool _canOpen = false;
    private bool _isOpen;

    public GameObject openSign;

    private void OnEnable()
    {
        if (_currentBagData == null)
        {
            _currentBagData = Instantiate(bagSOTemplate);
        }
    }

    private void Update()
    {
        if (!_isOpen && _canOpen && Input.GetMouseButtonDown(1))
        {
            /* 打开背包 */
            EventHandler.CallBaseBagOpenEvent(SlotType.Box, _currentBagData);
            _isOpen = true;
        }

        if ((!_canOpen && _isOpen) || (_isOpen && Input.GetKeyDown(KeyCode.Escape)))
        {
            EventHandler.CallBaseBagCloseEvent(SlotType.Box, _currentBagData);
            _isOpen = false;
        }
    }

    #region 碰撞检测

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player") && !_isOpen)
        {
            _canOpen = true;
            openSign.SetActive(true);
        }
    }

    private void OnTriggerExit2D(Collider2D other)
    {
        if (other.CompareTag("Player") && !_isOpen)
        {
            _canOpen = false;
            openSign.SetActive(false);
        }
    }

    #endregion
}

然后需要对应修改打开和关闭UI的代码InventoryUI.cs,为其打开背包的事件添加对应的背包格子Slot

我们设置的每一个背包格子都是特殊的种类,因此要设置不同的预制体来控制生成的背包格子类型

1
2
3
4
5
6
7
// 在inventory.cs中的  OnBaseBagOpenEvent
GameObject slotPrefab = slotType switch
{
    SlotType.Shop => shopSlotPrefab,
    SlotType.Box => boxSlotPrefab,
    _ => null
};

由于每个储物箱内容的生成需要在储物箱一开始被创建的时候就被赋值,而创建过程是在ItemManagerOnEnable中被使用的,因此其只能在Box的初始化赋值也只能OnEnable中执行,否则通过事件去获得Box存储的数据则会出错。

完成存储箱和玩家的背包的数据交换,以及切换场景的物品保存

由于需要实现存储箱和玩家背包的拖拽数据交换,因此需要在对应的SlotUI.CS中去处理拖拽完成方法OnEndDrag

如果要玩家背包与储物箱数据进行交换,首先要知道是哪一个储物箱的数据,因此需要在InventoryManager中注册打开储物箱时候的事件,用于获得打开的储物箱的具体数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/// <summary>
/// 打开某一类型的背包的时候,绑定数据
/// </summary>
/// <param name="slotType">打开背包的种类</param>
/// <param name="bagSO">背包的具体数据</param>
private void OnBaseBagOpenEvent(SlotType slotType, InventoryBag_SO bagSO)
{
    if (slotType == SlotType.Box)
    {
        _currentBoxBag = bagSO;
    }
}

然后需要设定新的交换函数,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/// <summary>
/// 交换背包数据
/// </summary>
/// <param name="fromIndex">交换来源的Index</param>
/// <param name="locationFrom">交换来源的类型</param>
/// <param name="targetIndex">交换目标的Index</param>
/// <param name="locationTarget">交换目标的类型</param>
public void SwapPlayerBagItem(int fromIndex, InventoryLocation locationFrom, int targetIndex, InventoryLocation locationTarget)
{
    
    List<InventoryItem> fromItemList = GetItemList(locationFrom);
    List<InventoryItem> targetItemList = GetItemList(locationTarget);

    

    if (targetIndex >= GetItemList(locationTarget).Count) return;
    InventoryItem fromItem = fromItemList[fromIndex];
    InventoryItem targetItem = targetItemList[targetIndex];
    
    if (targetItem.itemAmount == 0)
    {
        targetItemList[targetIndex] = fromItem;
        fromItemList[fromIndex] = new InventoryItem();

    }
    // 两个物品相同
    else if (fromItem.itemID == targetItem.itemID)
    {
        targetItem.itemAmount += fromItem.itemAmount;
        targetItemList[targetIndex] = targetItem;
        fromItemList[fromIndex] = new InventoryItem();
    }
    else    // 两个不同的物品
    {
        fromItemList[fromIndex] = targetItem;
        targetItemList[targetIndex] = fromItem;
    }

    EventHandler.CallUpdateInventoryUI(locationFrom, fromItemList);
    EventHandler.CallUpdateInventoryUI(locationTarget, targetItemList);
}
1
2
3
4
5
6
// slotui.cs 中修改OnEndDrag
else if (slotType != SlotType.Shop && targetSlot.slotType != SlotType.Shop &&
         slotType != targetSlot.slotType)
{
    InventoryManager.Instance.SwapPlayerBagItem(slotIndex, SlotLocation, targetSlot.slotIndex, targetSlot.SlotLocation);
}

根据slot的类型来获得当前交换的类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public InventoryLocation SlotLocation
{
 get
 {
     return slotType switch
     {
         SlotType.Bag => InventoryLocation.Player,
         SlotType.Shop => InventoryLocation.Shop,
         SlotType.Box => InventoryLocation.Box
     };
 }
}
  • 切换场景时,保存存储箱中的数据

方法类似于保存场景数据,由于我们是保存存储箱的数据,因此将该部分内容存储到InventoryManager

由于每个场景可能有多个箱子,因此每个场景中每个箱子存储的数据需要是独立的,因此每个key需要以场景加序号的方式进行设置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 在InventoryManager.cs中保存箱子的具体数据,并设置查询方法
/* 保存存储箱的内容 */
private Dictionary<string, List<InventoryItem>> _boxDataDict = new Dictionary<string, List<InventoryItem>>();
public int BoxDataDictCount => _boxDataDict.Count;

/// <summary>
/// 根据key,找出存储箱总控字典中存储的数据
/// </summary>
/// <param name="key"></param>
/// <returns>key对应的存储箱数据</returns>
public List<InventoryItem> GetBoxDataListFromDick(string key)
{
    if (_boxDataDict.ContainsKey(key))
        return _boxDataDict[key];
    else
        return null;
}

/// <summary>
///  将每个储物箱的数据添加进入字典中
/// </summary>
/// <param name="boxData"></param>
public void AddDataToBoxDict(Box boxData)
{
    string key = boxData.name + boxData.boxIndex;
    _boxDataDict.Add(key, boxData.GetBoxDataSO().inventoryItemList);
}

Key使用箱子的名称和其序号叠加的方式来设置,因此每当创建一个箱子的时候都需要为其设置序号,

1
2
3
4
5
6
// itemManager.cs 的OnBuildFurnitureEvent方法中添加
if (BuildItem.GetComponent<Box>())
{
    BuildItem.GetComponent<Box>().boxIndex = InventoryManager.Instance.BoxDataDictCount;
    BuildItem.GetComponent<Box>().BoxDataInit();
}

当存在序号以后,为了保证每次重建箱子的时候,能得到对应箱子的数据,因此箱子每次重建都需要对其中的数据进行初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// box.cs中对箱子的数据进行初始化
public void BoxDataInit(int curIndex)
{
    boxIndex = curIndex;
    string key = this.name + boxIndex;
    if (InventoryManager.Instance.GetBoxDataListFromDick(key) != null)
    {
        _currentBagData.inventoryItemList = InventoryManager.Instance.GetBoxDataListFromDick(key);
    }
    else    // 刷新时新建箱子
    {
        InventoryManager.Instance.AddDataToBoxDict(this);
    }
}

但是此时由于我们的每次切换场景的重建蓝图建筑都是直接调用的建造方法,在该方法中,如果检测到是一个Box,则会将其序号设置为dict的大小,而每次切换数据字典并不会改变,因此每次切换场景字典都在累加。

为了解决这一问题,需要将ReBuildSceneFurniture()进行改写,且保证我们新建一个初始化一个box的时候,是先得到其对应的index,然后进行赋值

1
2
3
4
public void BoxDataInit(int curIndex)
{
    boxIndex = curIndex;
    string key = this.name + boxIndex;

修改重建函数:

由于我们重建方法是遍历所有已经保存的SceneFurniture,因此我们需要在该class增加一个箱子的序号,并在每次生成的时候进行保存,那么在重建的时候,直接调用即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
foreach (SceneFurniture item in currentFurnitureList)
{

    BluePrintDetails bluePrintDetails =
        InventoryManager.Instance.bluePrintDataList.GetBluePrintDetails(item.bluePrintID);
    var BuildItem = Instantiate(bluePrintDetails.buildPrefab, item.itemPos.ToVector3(), Quaternion.identity, _itemParent.transform);
    /* 如果创建的物品是一个Box,那么需要为其设置Index值. 并初始化 */
    if (BuildItem.GetComponent<Box>())
    {
        BuildItem.GetComponent<Box>().BoxDataInit(item.boxIndex);
    }
}
1
2
3
4
5
6
7
8
9
// itemManager.cs的获取所有场景蓝图建筑信息GetAllSceneFurniture()的修改内容
foreach (Furniture furniture in FindObjectsOfType<Furniture>()){
    //....
   if (furniture.GetComponent<Box>())
    {
        newSceneFurniture.boxIndex = furniture.GetComponent<Box>().boxIndex;
    } 
}
    
Built with Hugo
Theme Stack designed by Jimmy