Tworzenie ekwipunku i systemu wytwarzania przedmiotów w Unity
W tym samouczku pokażę, jak stworzyć w stylu Minecraft system ekwipunku i wytwarzania przedmiotów w Unity.
Tworzenie przedmiotów w grach wideo to proces łączenia określonych (zwykle prostszych) przedmiotów w bardziej złożone przedmioty o nowych i ulepszonych właściwościach. Na przykład połączenie drewna i kamienia w kilof lub połączenie blachy i drewna w miecz.
Poniższy system rzemieślniczy jest przyjazny dla urządzeń mobilnych i w pełni zautomatyzowany, co oznacza, że będzie działał z dowolnym układem interfejsu użytkownika i będzie miał możliwość tworzenia niestandardowych przepisów rzemieślniczych.
Krok 1: Skonfiguruj interfejs rzemieślniczy
Zaczynamy od skonfigurowania interfejsu rzemieślniczego:
- Utwórz nowe płótno (Unity Górny pasek zadań: GameObject -> UI -> Płótno)
- Utwórz nowy obraz, klikając prawym przyciskiem myszy obiekt Canvas -> Interfejs użytkownika -> Obraz
- Zmień nazwę obiektu obrazu na "CraftingPanel" i zmień jego obraz źródłowy na domyślny "UISprite"
- Zmień wartości "CraftingPanel" RectTransform na (Poz X: 0 Poz Y: 0 Szerokość: 410 Wysokość: 365)
- Utwórz dwa obiekty wewnątrz "CraftingPanel" (kliknij prawym przyciskiem myszy CraftingPanel -> Utwórz pusty, 2 razy)
- Zmień nazwę pierwszego obiektu na "CraftingSlots" i zmień jego wartości RectTransform na („Wyrównanie do lewego górnego” Pivot X: 0 Pivot Y: 1 Pos X: 50 Pos Y: -35 Szerokość: 140 Wysokość: 140). Obiekt ten będzie zawierał miejsca do wytwarzania.
- Zmień nazwę drugiego obiektu na "PlayerSlots" i zmień jego wartości RectTransform na („Rozciągnij górą w poziomie” Obrót X: 0,5 Obrót Y: 1 Lewy: 0 Poz. Y: -222 Prawy: 0 Wysokość: 100). Obiekt ten będzie zawierał miejsca dla graczy.
Tytuł sekcji:
- Utwórz nowy tekst, klikając prawym przyciskiem myszy obiekt "PlayerSlots" -> Interfejs użytkownika -> Tekst i zmień jego nazwę na "SectionTitle"
- Zmień wartości "SectionTitle" RectTransform na („Wyrównanie do góry i lewej strony” Pivot X: 0 Pivot Y: 0 Pos X: 5 Pos Y: 0 Szerokość: 160 Wysokość: 30)
- Zmień tekst "SectionTitle" na "Inventory" i ustaw jego rozmiar czcionki na 18, wyrównanie do lewego środka i kolor na (0,2, 0,2, 0,2, 1)
- Zduplikuj obiekt "SectionTitle", zmień jego tekst na "Crafting" i przenieś go pod obiekt "CraftingSlots", a następnie ustaw te same wartości RectTransform co poprzedni "SectionTitle".
Miejsce rzemieślnicze:
Miejsce wytwarzania będzie składać się z obrazu tła, obrazu przedmiotu i tekstu licznika:
- Utwórz nowy obraz, klikając prawym przyciskiem myszy obiekt Canvas -> Interfejs użytkownika -> Obraz
- Zmień nazwę nowego obrazu na "slot_template", ustaw jego wartości RectTransform na (Post X: 0 Pos Y: 0 Szerokość: 40 Wysokość: 40) i zmień jego kolor na (0,32, 0,32, 0,32, 0,8)
- Zduplikuj "slot_template" i zmień jego nazwę na "Item", przenieś go do obiektu "slot_template", zmień jego wymiary RectTransform na (Width: 30 Height: 30) i Color na (1, 1, 1, 1)
- Utwórz nowy tekst, klikając prawym przyciskiem myszy obiekt "slot_template" -> Interfejs użytkownika -> Tekst i zmień jego nazwę na "Count"
- Zmień wartości "Count" RectTransform na („Wyrównaj do prawego dołu” Pivot X: 1 Pivot Y: 0 Pos X: 0 Pos Y: 0 Szerokość: 30 Wysokość: 30)
- Ustaw "Count" Tekst na losową liczbę (np. 12), Styl czcionki na Pogrubiony, Rozmiar czcionki na 14, Wyrównanie do prawego dołu i Kolor na (1, 1, 1, 1)
- Dodaj komponent Cień do "Count" Tekst i ustaw Kolor efektu na (0, 0, 0, 0.5)
Efekt końcowy powinien wyglądać następująco:
Miejsce na wynik (będzie używane do tworzenia wyników):
- Zduplikuj obiekt "slot_template" i zmień jego nazwę na "result_slot_template"
- Zmień szerokość i wysokość "result_slot_template" na 50
Przycisk tworzenia i dodatkowa grafika:
- Utwórz nowy przycisk, klikając prawym przyciskiem myszy obiekt "CraftingSlots" -> Interfejs użytkownika -> Przycisk i zmień jego nazwę na "CraftButton"
- Ustaw wartości "CraftButton" RectTransform na („Wyrównanie do środkowego lewego” Pivot X: 1 Pivot Y: 0,5 Pos X: 0 Pos Y: 0 Szerokość: 40 Wysokość: 40)
- Zmień tekst "CraftButton" na "Craft"
- Utwórz nowy obraz, klikając prawym przyciskiem myszy obiekt "CraftingSlots" -> Interfejs użytkownika -> Obraz i zmień jego nazwę na "Arrow"
- Ustaw wartości "Arrow" RectTransform na („Wyrównanie do środkowego prawego” Pivot X: 0 Pivot Y: 0,5 Pos X: 10 Pos Y: 0 Szerokość: 30 Wysokość: 30)
W przypadku obrazu źródłowego możesz użyć poniższego obrazu (kliknij prawym przyciskiem myszy -> Zapisz jako..., aby go pobrać). Po zaimportowaniu ustaw typ tekstury na "Sprite (2D and UI)" i tryb filtra na "Point (no filter)"
- Kliknij prawym przyciskiem myszy "CraftingSlots" -> Utwórz pusty i zmień jego nazwę na "ResultSlot", obiekt ten będzie zawierał miejsce na wynik
- Ustaw wartości "ResultSlot" RectTransform na („Wyrównaj do środkowego prawego” Pivot X: 0 Pivot Y: 0,5 Pos X: 50 Pos Y: 0 Szerokość: 50 Wysokość: 50)
Konfiguracja interfejsu użytkownika jest gotowa.
Krok 2: System tworzenia programów
Ten system rzemieślniczy będzie składał się z 2 skryptów, SC_ItemCrafting.cs i SC_SlotTemplate.cs
- Utwórz nowy skrypt, nazwij go "SC_ItemCrafting" i wklej w nim poniższy kod:
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);
}
}
}
}
- Utwórz nowy skrypt, nadaj mu nazwę "SC_SlotTemplate" i wklej w nim poniższy kod:
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();
}
}
Przygotowanie szablonów slotów:
- Dołącz skrypt SC_SlotTemplate do obiektu "slot_template" i przypisz jego zmienne (Komponent obrazu w tym samym obiekcie trafia do zmiennej "Container", dziecko "Item" Obraz trafia do zmiennej "Item", a dziecko "Count" Tekst trafia do zmiennej "Count")
- Powtórz ten sam proces dla obiektu "result_slot_template" (dołącz do niego skrypt SC_SlotTemplate i przypisz zmienne w ten sam sposób).
Przygotowanie systemu rzemieślniczego:
- Dołącz skrypt SC_ItemCrafting do obiektu Canvas i przypisz jego zmienne (obiekt "PlayerSlots" trafia do zmiennej "Player Slots Container", "CraftingSlots" obiekt trafia do zmiennej "Crafting Slots Container", "ResultSlot" obiekt trafia do "Result Slot Container" zmienna, "CraftButton" Obiekt trafia do zmiennej "Craft Button", "slot_template" Obiekt z dołączonym skryptem SC_SlotTemplate trafia do zmiennej "Slot Template", a obiekt "result_slot_template" z dołączonym skryptem SC_SlotTemplate trafia do zmiennej "Result Slot Template"):
Jak już zauważyłeś, istnieją dwie puste tablice o nazwach "Player Slots" i "Items". "Player Slots" będzie zawierać liczbę dostępnych miejsc (z Przedmiotem lub pustych), a "Items" będzie zawierać wszystkie dostępne przedmioty wraz z ich recepturami (opcjonalnie).
Konfigurowanie elementów:
Sprawdź poniższe sprite'y (w moim przypadku będę miał 5 pozycji):
(głaz)
(diament)
(drewno)
(miecz)
(Diamentowy miecz)
- Pobierz każdego duszka (kliknij prawym przyciskiem myszy -> Zapisz jako...) i zaimportuj go do swojego projektu (w ustawieniach importu ustaw ich typ tekstury na "Sprite (2D and UI)" i tryb filtra na "Point (no filter)"
- W SC_ItemCrafting zmień Items Size na 5 i przypisz każdego duszka do zmiennej Item Sprite.
"Stackable" zmienna kontroluje, czy przedmioty mogą być układane razem w jednym miejscu (np. możesz zezwolić na układanie tylko prostych materiałów, takich jak kamień, diament i drewno).
"Craft Recipe" zmienna kontrola, czy ten przedmiot można wytworzyć (pusty oznacza, że nie można go wytworzyć)
- Dla "Player Slots" ustaw Array Size na 27 (najlepiej pasuje do bieżącego panelu rzemieślniczego, ale możesz ustawić dowolną liczbę).
Po naciśnięciu przycisku Play zauważysz, że sloty zostały poprawnie zainicjowane, ale nie ma w nich żadnych elementów:
Aby dodać przedmiot do każdego slotu, będziemy musieli przypisać Sprite'a przedmiotu do zmiennej "Item Sprite" i ustawić "Item Count" na dowolną liczbę dodatnią (wszystko poniżej 1 i/lub przedmioty, których nie można układać w stosy, będą interpretowane jako 1):
- Przypisz duszka "rock" do Elementu 0 / "Item Count" 14, duszka "wood" do Elementu 1 / "Item Count" 8, duszka "diamond" do Elementu 2 / "Item Count" 8 (Upewnij się, że duszki są takie same jak w tablicy "Items", inaczej to nie zadziała).
Przedmioty powinny teraz pojawić się w Slotach Graczy, możesz zmienić ich położenie klikając na przedmiot, a następnie klikając na miejsce, do którego chcesz go przenieść.
Przepisy rzemieślnicze:
Przepisy rzemieślnicze pozwalają na stworzenie przedmiotu poprzez połączenie innych przedmiotów w określonej kolejności:
Format receptury wytwarzania jest następujący: [nazwa_ducha_przedmiotu]([liczba przedmiotów])*opcjonalnie... powtórzone 9 razy, oddzielone przecinkiem (,)
Łatwym sposobem na odkrycie przepisu jest naciśnięcie przycisku Odtwórz, następnie ułożenie przedmiotów w kolejności, w jakiej chcesz je wytworzyć, następnie naciśnięcie "Craft", a następnie naciśnięcie (Ctrl + Shift + C), aby otworzyć konsolę Unity i zobaczyć nowo wydrukowana linia (możesz kliknąć "Craft" wiele razy, aby ponownie wydrukować linię), wydrukowana linia jest przepisem rzemieślniczym.
Na przykład poniższa kombinacja odpowiada temu przepisowi: rock,,rock,,rock,,rock,,wood (UWAGA: w Twoim przypadku może być inaczej, jeśli Twoje duszki mają różne nazwy).
Powyższy przepis wykorzystamy do stworzenia miecza.
- Skopiuj wydrukowaną linię i w tablicy "Items" wklej ją do zmiennej "Craft Recipe" pod "sword" Pozycja:
Teraz, powtarzając tę samą kombinację, powinieneś być w stanie stworzyć miecz.
Przepis na diamentowy miecz jest taki sam, ale zamiast kamienia jest to diament:
System rzemieślniczy jest już gotowy.