在 Unity 中制作库存和物品制作系统

在本教程中,我将展示如何在 Unity 中制作 Minecraft 风格的 库存和物品制作系统。

视频游戏中的物品制作是将特定(通常更简单)物品组合成更复杂物品的过程,具有新的和增强的属性。例如,将木头和石头组合成镐,或者将金属板和木头组合成剑。

下面的制作系统是移动友好并且完全自动化,这意味着它将与任何UI布局一起工作,并且能够创建自定义制作配方。

Sharp Coder 视频播放器

第 1 步:设置制作 UI

我们首先设置制作 UI:

  • 创建一个新的画布(Unity 顶部任务栏:游戏对象 -> UI -> 画布)
  • 通过右键单击画布对象 -> UI -> 图像来创建新图像
  • 将图像对象重命名为 "CraftingPanel" 并将其源图像更改为默认值 "UISprite"
  • 将 "CraftingPanel" RectTransform 值更改为 (Pos X: 0 Pos Y: 0 宽度: 410 高度: 365)

  • 在 "CraftingPanel" 内创建两个对象(右键单击 CraftingPanel -> Create Empty,2 次)
  • 将第一个对象重命名为 "CraftingSlots" 并将其 RectTransform 值更改为(“左上对齐” Pivot X: 0 Pivot Y: 1 Pos X: 50 Pos Y: -35 Width: 140 Height: 140)。该对象将包含制作槽。
  • 将第二个对象重命名为 "PlayerSlots" 并将其 RectTransform 值更改为 ("Top Stretch Horizo​​ntally" Pivot X: 0.5 Pivot Y: 1 Left: 0 Pos Y: -222 Right: 0 Height: 100)。该对象将包含玩家槽位。

章节标题:

  • 通过右键单击 "PlayerSlots" 对象 -> UI -> 文本创建新文本并将其重命名为 "SectionTitle"
  • 将 "SectionTitle" RectTransform 值更改为(“左上对齐” Pivot X: 0 Pivot Y: 0 Pos X: 5 Pos Y: 0 Width: 160 Height: 30)
  • 将 "SectionTitle" 文本更改为 "Inventory" 并将其字体大小设置为 18,对齐方式设置为左中,颜色设置为 (0.2, 0.2, 0.2, 1)
  • 复制 "SectionTitle" 对象,将其文本更改为 "Crafting" 并将其移动到 "CraftingSlots" 对象下,然后设置与之前的 "SectionTitle" 相同的 RectTransform 值。

制作槽:

制作槽将由背景图像、物品图像和计数文本组成:

  • 通过右键单击画布对象 -> UI -> 图像来创建新图像
  • 将新图像重命名为 "slot_template",将其 RectTransform 值设置为 (Post X: 0 Pos Y: 0 Width: 40 Height: 40),并将其颜色更改为 (0.32, 0.32, 0.32, 0.8)
  • 复制 "slot_template" 并将其重命名为 "Item",将其移动到 "slot_template" 对象内,将其 RectTransform 尺寸更改为 (Width: 30 Height: 30),将 Color 更改为 (1, 1, 1, 1)
  • 通过右键单击 "slot_template" 对象 -> UI -> 文本创建新文本并将其重命名为 "Count"
  • 将 "Count" RectTransform 值更改为(“右下对齐” Pivot X: 1 Pivot Y: 0 Pos X: 0 Pos Y: 0 Width: 30 Height: 30)
  • 将 "Count" 文本设置为随机数(例如 12),将字体样式设置为粗体,将字体大小设置为 14,将对齐方式设置为右下,将颜色设置为 (1, 1, 1, 1)
  • 将阴影组件添加到 "Count" 文本并将效果颜色设置为 (0, 0, 0, 0.5)

最终结果应该是这样的:

结果槽(将用于制作结果):

  • 复制 "slot_template" 对象并将其重命名为 "result_slot_template"
  • 将 "result_slot_template" 的宽度和高度更改为 50

制作按钮和附加图形:

  • 右键单击 "CraftingSlots" 对象 -> UI -> 按钮创建一个新按钮并将其重命名为 "CraftButton"
  • 将 "CraftButton" RectTransform 值设置为(“中左对齐” Pivot X:1 Pivot Y:0.5 Pos X:0 Pos Y:0 宽度:40 高度:40)
  • 将 "CraftButton" 的文本更改为 "Craft"

  • 右键单击 "CraftingSlots" 对象 -> UI -> 图像创建一个新图像并将其重命名为 "Arrow"
  • 将 "Arrow" RectTransform 值设置为(“中右对齐” Pivot X: 0 Pivot Y: 0.5 Pos X: 10 Pos Y: 0 Width: 30 Height: 30)

对于源图像,您可以使用下面的图像(右键单击 -> 另存为..下载)。导入后将其纹理类型设置为 "Sprite (2D and UI)" 并将过滤模式设置为 "Point (no filter)"

箭头向右图标像素

  • 右键单击 "CraftingSlots" -> Create Empty 并将其重命名为 "ResultSlot",该对象将包含结果槽
  • 将 "ResultSlot" RectTransform 值设置为(“中右对齐” Pivot X: 0 Pivot Y: 0.5 Pos X: 50 Pos Y: 0 Width: 50 Height: 50)

UI 设置已准备就绪。

第二步:程序制作系统

该制作系统将由 2 个脚本组成:SC_ItemCrafting.cs 和 SC_SlotTemplate.cs

  • 创建 一个新脚本,将其命名为 "SC_ItemCrafting",然后将以下代码粘贴到其中:

SC_ItemCrafting.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SC_ItemCrafting : MonoBehaviour
{
    public RectTransform playerSlotsContainer;
    public RectTransform craftingSlotsContainer;
    public RectTransform resultSlotContainer;
    public Button craftButton;
    public SC_SlotTemplate slotTemplate;
    public SC_SlotTemplate resultSlotTemplate;

    [System.Serializable]
    public class SlotContainer
    {
        public Sprite itemSprite; //Sprite of the assigned item (Must be the same sprite as in items array), or leave null for no item
        public int itemCount; //How many items in this slot, everything equal or under 1 will be interpreted as 1 item
        [HideInInspector]
        public int tableID;
        [HideInInspector]
        public SC_SlotTemplate slot;
    }

    [System.Serializable]
    public class Item
    {
        public Sprite itemSprite;
        public bool stackable = false; //Can this item be combined (stacked) together?
        public string craftRecipe; //Item Keys that are required to craft this item, separated by comma (Tip: Use Craft Button in Play mode and see console for printed recipe)
    }

    public SlotContainer[] playerSlots;
    SlotContainer[] craftSlots = new SlotContainer[9];
    SlotContainer resultSlot = new SlotContainer();
    //List of all available items
    public Item[] items;

    SlotContainer selectedItemSlot = null;

    int craftTableID = -1; //ID of table where items will be placed one at a time (ex. Craft table)
    int resultTableID = -1; //ID of table from where we can take items, but cannot place to

    ColorBlock defaultButtonColors;

    // Start is called before the first frame update
    void Start()
    {
        //Setup slot element template
        slotTemplate.container.rectTransform.pivot = new Vector2(0, 1);
        slotTemplate.container.rectTransform.anchorMax = slotTemplate.container.rectTransform.anchorMin = new Vector2(0, 1);
        slotTemplate.craftingController = this;
        slotTemplate.gameObject.SetActive(false);
        //Setup result slot element template
        resultSlotTemplate.container.rectTransform.pivot = new Vector2(0, 1);
        resultSlotTemplate.container.rectTransform.anchorMax = resultSlotTemplate.container.rectTransform.anchorMin = new Vector2(0, 1);
        resultSlotTemplate.craftingController = this;
        resultSlotTemplate.gameObject.SetActive(false);

        //Attach click event to craft button
        craftButton.onClick.AddListener(PerformCrafting);
        //Save craft button default colors
        defaultButtonColors = craftButton.colors;

        //InitializeItem Crafting Slots
        InitializeSlotTable(craftingSlotsContainer, slotTemplate, craftSlots, 5, 0);
        UpdateItems(craftSlots);
        craftTableID = 0;

        //InitializeItem Player Slots
        InitializeSlotTable(playerSlotsContainer, slotTemplate, playerSlots, 5, 1);
        UpdateItems(playerSlots);

        //InitializeItemResult Slot
        InitializeSlotTable(resultSlotContainer, resultSlotTemplate, new SlotContainer[] { resultSlot }, 0, 2);
        UpdateItems(new SlotContainer[] { resultSlot });
        resultTableID = 2;

        //Reset Slot element template (To be used later for hovering element)
        slotTemplate.container.rectTransform.pivot = new Vector2(0.5f, 0.5f);
        slotTemplate.container.raycastTarget = slotTemplate.item.raycastTarget = slotTemplate.count.raycastTarget = false;
    }

    void InitializeSlotTable(RectTransform container, SC_SlotTemplate slotTemplateTmp, SlotContainer[] slots, int margin, int tableIDTmp)
    {
        int resetIndex = 0;
        int rowTmp = 0;
        for (int i = 0; i < slots.Length; i++)
        {
            if (slots[i] == null)
            {
                slots[i] = new SlotContainer();
            }
            GameObject newSlot = Instantiate(slotTemplateTmp.gameObject, container.transform);
            slots[i].slot = newSlot.GetComponent<SC_SlotTemplate>();
            slots[i].slot.gameObject.SetActive(true);
            slots[i].tableID = tableIDTmp;

            float xTmp = (int)((margin + slots[i].slot.container.rectTransform.sizeDelta.x) * (i - resetIndex));
            if (xTmp + slots[i].slot.container.rectTransform.sizeDelta.x + margin > container.rect.width)
            {
                resetIndex = i;
                rowTmp++;
                xTmp = 0;
            }
            slots[i].slot.container.rectTransform.anchoredPosition = new Vector2(margin + xTmp, -margin - ((margin + slots[i].slot.container.rectTransform.sizeDelta.y) * rowTmp));
        }
    }

    //Update Table UI
    void UpdateItems(SlotContainer[] slots)
    {
        for (int i = 0; i < slots.Length; i++)
        {
            Item slotItem = FindItem(slots[i].itemSprite);
            if (slotItem != null)
            {
                if (!slotItem.stackable)
                {
                    slots[i].itemCount = 1;
                }
                //Apply total item count
                if (slots[i].itemCount > 1)
                {
                    slots[i].slot.count.enabled = true;
                    slots[i].slot.count.text = slots[i].itemCount.ToString();
                }
                else
                {
                    slots[i].slot.count.enabled = false;
                }
                //Apply item icon
                slots[i].slot.item.enabled = true;
                slots[i].slot.item.sprite = slotItem.itemSprite;
            }
            else
            {
                slots[i].slot.count.enabled = false;
                slots[i].slot.item.enabled = false;
            }
        }
    }

    //Find Item from the items list using sprite as reference
    Item FindItem(Sprite sprite)
    {
        if (!sprite)
            return null;

        for (int i = 0; i < items.Length; i++)
        {
            if (items[i].itemSprite == sprite)
            {
                return items[i];
            }
        }

        return null;
    }

    //Find Item from the items list using recipe as reference
    Item FindItem(string recipe)
    {
        if (recipe == "")
            return null;

        for (int i = 0; i < items.Length; i++)
        {
            if (items[i].craftRecipe == recipe)
            {
                return items[i];
            }
        }

        return null;
    }

    //Called from SC_SlotTemplate.cs
    public void ClickEventRecheck()
    {
        if (selectedItemSlot == null)
        {
            //Get clicked slot
            selectedItemSlot = GetClickedSlot();
            if (selectedItemSlot != null)
            {
                if (selectedItemSlot.itemSprite != null)
                {
                    selectedItemSlot.slot.count.color = selectedItemSlot.slot.item.color = new Color(1, 1, 1, 0.5f);
                }
                else
                {
                    selectedItemSlot = null;
                }
            }
        }
        else
        {
            SlotContainer newClickedSlot = GetClickedSlot();
            if (newClickedSlot != null)
            {
                bool swapPositions = false;
                bool releaseClick = true;

                if (newClickedSlot != selectedItemSlot)
                {
                    //We clicked on the same table but different slots
                    if (newClickedSlot.tableID == selectedItemSlot.tableID)
                    {
                        //Check if new clicked item is the same, then stack, if not, swap (Unless it's a crafting table, then do nothing)
                        if (newClickedSlot.itemSprite == selectedItemSlot.itemSprite)
                        {
                            Item slotItem = FindItem(selectedItemSlot.itemSprite);
                            if (slotItem.stackable)
                            {
                                //Item is the same and is stackable, remove item from previous position and add its count to a new position
                                selectedItemSlot.itemSprite = null;
                                newClickedSlot.itemCount += selectedItemSlot.itemCount;
                                selectedItemSlot.itemCount = 0;
                            }
                            else
                            {
                                swapPositions = true;
                            }
                        }
                        else
                        {
                            swapPositions = true;
                        }
                    }
                    else
                    {
                        //Moving to different table
                        if (resultTableID != newClickedSlot.tableID)
                        {
                            if (craftTableID != newClickedSlot.tableID)
                            {
                                if (newClickedSlot.itemSprite == selectedItemSlot.itemSprite)
                                {
                                    Item slotItem = FindItem(selectedItemSlot.itemSprite);
                                    if (slotItem.stackable)
                                    {
                                        //Item is the same and is stackable, remove item from previous position and add its count to a new position
                                        selectedItemSlot.itemSprite = null;
                                        newClickedSlot.itemCount += selectedItemSlot.itemCount;
                                        selectedItemSlot.itemCount = 0;
                                    }
                                    else
                                    {
                                        swapPositions = true;
                                    }
                                }
                                else
                                {
                                    swapPositions = true;
                                }
                            }
                            else
                            {
                                if (newClickedSlot.itemSprite == null || newClickedSlot.itemSprite == selectedItemSlot.itemSprite)
                                {
                                    //Add 1 item from selectedItemSlot
                                    newClickedSlot.itemSprite = selectedItemSlot.itemSprite;
                                    newClickedSlot.itemCount++;
                                    selectedItemSlot.itemCount--;
                                    if (selectedItemSlot.itemCount <= 0)
                                    {
                                        //We placed the last item
                                        selectedItemSlot.itemSprite = null;
                                    }
                                    else
                                    {
                                        releaseClick = false;
                                    }
                                }
                                else
                                {
                                    swapPositions = true;
                                }
                            }
                        }
                    }
                }

                if (swapPositions)
                {
                    //Swap items
                    Sprite previousItemSprite = selectedItemSlot.itemSprite;
                    int previousItemConunt = selectedItemSlot.itemCount;

                    selectedItemSlot.itemSprite = newClickedSlot.itemSprite;
                    selectedItemSlot.itemCount = newClickedSlot.itemCount;

                    newClickedSlot.itemSprite = previousItemSprite;
                    newClickedSlot.itemCount = previousItemConunt;
                }

                if (releaseClick)
                {
                    //Release click
                    selectedItemSlot.slot.count.color = selectedItemSlot.slot.item.color = Color.white;
                    selectedItemSlot = null;
                }

                //Update UI
                UpdateItems(playerSlots);
                UpdateItems(craftSlots);
                UpdateItems(new SlotContainer[] { resultSlot });
            }
        }
    }

    SlotContainer GetClickedSlot()
    {
        for (int i = 0; i < playerSlots.Length; i++)
        {
            if (playerSlots[i].slot.hasClicked)
            {
                playerSlots[i].slot.hasClicked = false;
                return playerSlots[i];
            }
        }

        for (int i = 0; i < craftSlots.Length; i++)
        {
            if (craftSlots[i].slot.hasClicked)
            {
                craftSlots[i].slot.hasClicked = false;
                return craftSlots[i];
            }
        }

        if (resultSlot.slot.hasClicked)
        {
            resultSlot.slot.hasClicked = false;
            return resultSlot;
        }

        return null;
    }

    void PerformCrafting()
    {
        string[] combinedItemRecipe = new string[craftSlots.Length];

        craftButton.colors = defaultButtonColors;

        for (int i = 0; i < craftSlots.Length; i++)
        {
            Item slotItem = FindItem(craftSlots[i].itemSprite);
            if (slotItem != null)
            {
                combinedItemRecipe[i] = slotItem.itemSprite.name + (craftSlots[i].itemCount > 1 ? "(" + craftSlots[i].itemCount + ")" : "");
            }
            else
            {
                combinedItemRecipe[i] = "";
            }
        }

        string combinedRecipe = string.Join(",", combinedItemRecipe);
        print(combinedRecipe);

        //Search if recipe match any of the item recipe
        Item craftedItem = FindItem(combinedRecipe);
        if (craftedItem != null)
        {
            //Clear Craft slots
            for (int i = 0; i < craftSlots.Length; i++)
            {
                craftSlots[i].itemSprite = null;
                craftSlots[i].itemCount = 0;
            }

            resultSlot.itemSprite = craftedItem.itemSprite;
            resultSlot.itemCount = 1;

            UpdateItems(craftSlots);
            UpdateItems(new SlotContainer[] { resultSlot });
        }
        else
        {
            ColorBlock colors = craftButton.colors;
            colors.selectedColor = colors.pressedColor = new Color(0.8f, 0.55f, 0.55f, 1);
            craftButton.colors = colors;
        }
    }

    // Update is called once per frame
    void Update()
    {
        //Slot UI follow mouse position
        if (selectedItemSlot != null)
        {
            if (!slotTemplate.gameObject.activeSelf)
            {
                slotTemplate.gameObject.SetActive(true);
                slotTemplate.container.enabled = false;

                //Copy selected item values to slot template
                slotTemplate.count.color = selectedItemSlot.slot.count.color;
                slotTemplate.item.sprite = selectedItemSlot.slot.item.sprite;
                slotTemplate.item.color = selectedItemSlot.slot.item.color;
            }

            //Make template slot follow mouse position
            slotTemplate.container.rectTransform.position = Input.mousePosition;
            //Update item count
            slotTemplate.count.text = selectedItemSlot.slot.count.text;
            slotTemplate.count.enabled = selectedItemSlot.slot.count.enabled;
        }
        else
        {
            if (slotTemplate.gameObject.activeSelf)
            {
                slotTemplate.gameObject.SetActive(false);
            }
        }
    }
}
  • 创建一个新脚本,将其命名为 "SC_SlotTemplate",然后将以下代码粘贴到其中:

SC_SlotTemplate.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class SC_SlotTemplate : MonoBehaviour, IPointerClickHandler
{
    public Image container;
    public Image item;
    public Text count;

    [HideInInspector]
    public bool hasClicked = false;
    [HideInInspector]
    public SC_ItemCrafting craftingController;

    //Do this when the mouse is clicked over the selectable object this script is attached to.
    public void OnPointerClick(PointerEventData eventData)
    {
        hasClicked = true;
        craftingController.ClickEventRecheck();
    }
}

准备插槽模板:

  • SC_SlotTemplate 脚本附加到"slot_template" 对象,并分配其变量(同一对象上的图像组件转到"Container" 变量,子"Item" 图像转到"Item" 变量,子 "Count" 文本转到 "Count" 变量)
  • 对 "result_slot_template" 对象重复相同的过程(将 SC_SlotTemplate 脚本附加到它并以相同的方式分配变量)。

准备工艺系统:

  • SC_ItemCrafting 脚本附加到 Canvas 对象,并分配其变量(“PlayerSlots”对象转到 "Player Slots Container" 变量,"CraftingSlots" 对象转到 "Crafting Slots Container" 变量,"ResultSlot" 对象转到 *h51) * 变量,"CraftButton" 对象转到 "Craft Button" 变量,附加 SC_SlotTemplate 脚本的 "slot_template" 对象转到 "Slot Template" 变量,附加 SC_SlotTemplate 脚本的 "result_slot_template" 对象转到 "Result Slot Template" 变量):

正如您已经注意到的,有两个名为 "Player Slots" 和 "Items" 的空数组。"Player Slots" 将包含可用的插槽数量(有物品或空),而 "Items" 将包含所有可用物品及其配方(可选)。

设置项目:

检查下面的精灵(在我的例子中我有 5 个项目):

摇滚物品 (岩石)

钻石物品 (钻石)

木制品 (木头)

剑物品 (剑)

钻石剑 (钻石剑)

  • 下载每个精灵(右键单击 -> 另存为...)并将它们导入到您的项目中(在导入设置中将其纹理类型设置为 "Sprite (2D and UI)" 并将过滤模式设置为 "Point (no filter)"

  • 在 SC_ItemCrafting 中,将 Items Size 更改为 5,并将每个 sprite 分配给 Item Sprite 变量。

"Stackable" 变量控制物品是否可以堆叠到一个槽中(例如,您可能只想允许堆叠简单的材料,例如岩石、钻石和木材)。

"Craft Recipe" 变量控制该物品是否可以制作(空表示无法制作)

  • 对于 "Player Slots" 将数组大小设置为 27(最适合当前的制作面板,但您可以设置任何数字)。

当您按“播放”时,您会注意到插槽已正确初始化,但没有项目:

要将项目添加到每个插槽,我们需要将项目 Sprite 分配给 "Item Sprite" 变量,并将 "Item Count" 设置为任何正数(1 以下的所有内容和/或不可堆叠的项目将被解释为 1):

  • 将 "rock" 精灵分配给元素 0 / "Item Count" 14,将 "wood" 精灵分配给元素 1 / "Item Count" 8,将 "diamond" 精灵分配给元素 2 / "Item Count" 8(确保精灵与元素相同)位于 "Items" 数组中,否则无法工作)。

物品现在应该出现在玩家槽位中,您可以通过单击该物品来更改它们的位置,然后单击要将其移动到的槽位。

制作食谱:

制作配方允许您通过按特定顺序组合其他物品来创建物品:

制作配方的格式如下:[item_sprite_name]([item count])*可选...重复9次,以逗号(,)分隔

发现配方的一个简单方法是按“播放”,然后按照您想要制作的顺序放置物品,然后按 "Craft",之后,按 (Ctrl + Shift + C) 打开 Unity 控制台并查看新打印的一行(可以多次点击"Craft"重新打印该行),打印的行就是制作配方。

例如,下面的组合对应于这个配方:rock,,rock,,rock,,rock,,wood(注意:如果你的精灵有不同的名称,它可能会有所不同)。

剑物品制作配方

我们将使用上面的配方来制作一把剑。

  • 复制打印的行,然后在 "Items" 数组中将其粘贴到 "sword" 项下的 "Craft Recipe" 变量中:

现在,当重复相同的组合时,你应该能够制作一把剑。

钻石剑的配方是相同的,但它不是岩石,而是钻石:

钻石物品剑配方 Unity Inspector

Unity 库存系统和物品制作

制作系统现已准备就绪。

来源
📁ItemCrafting.unitypackage36.13 KB
推荐文章
在 Unity 中使用 UI 拖放编写简单的库存系统
如何在 Unity 中触发过场动画
Unity状态机简介
在 Unity 中实现对象池
在 Unity 中创建交互式对象
在 Unity 中实现动力学交互
在 Unity 中使用特定钥匙打开抽屉和橱柜