יצירת מלאי ומערכת יצירת פריטים באחדות
במדריך זה, אני אראה כיצד ליצור סגנון Minecraft מערכת מלאי ויצירת פריטים ב-Unity.
יצירת פריטים במשחקי וידאו היא תהליך של שילוב פריטים ספציפיים (בדרך כלל פשוטים יותר) לפריטים מורכבים יותר, עם מאפיינים חדשים ומשופרים. למשל, שילוב עץ ואבן לכוש, או שילוב פח ועץ לחרב.
מערכת היצירה שלהלן היא ידידותית לניידים ואוטומטית לחלוטין, כלומר היא תעבוד עם כל פריסת ממשק משתמש ועם היכולת ליצור מתכוני יצירה מותאמים אישית.
שלב 1: הגדר את ממשק המשתמש ליצירה
אנו מתחילים בהגדרת ממשק המשתמש ליצירה:
- צור קנבס חדש (Unity שורת המשימות העליונה: GameObject -> ממשק משתמש -> קנבס)
- צור תמונה חדשה על ידי לחיצה ימנית על אובייקט קנבס -> ממשק משתמש -> תמונה
- שנה את שם אובייקט התמונה ל-"CraftingPanel" ושנה את תמונת המקור שלו לברירת מחדל "UISprite"
- שנה את ערכי "CraftingPanel" RectTransform ל-(Pos X: 0 Pos Y: 0 רוחב: 410 גובה: 365)
- צור שני אובייקטים בתוך "CraftingPanel" (לחץ לחיצה ימנית על CraftingPanel -> צור ריק, 2 פעמים)
- שנה את שם האובייקט הראשון ל-"CraftingSlots" ושנה את ערכי ה-RectTransform שלו ל-"יישור למעלה משמאל" Pivot X: 0 Pivot Y: 1 Pos X: 50 Pos Y: -35 רוחב: 140 גובה: 140). אובייקט זה יכיל חריצי יצירה.
- שנה את שם האובייקט השני ל-"PlayerSlots" ושנה את ערכי ה-RectTransform שלו ל- ("Top Stretch Horizontally" Pivot X: 0.5 Pivot Y: 1 Left: 0 Pos Y: -222 Right: 0 Height: 100). אובייקט זה יכיל משבצות שחקן.
כותרת המדור:
- צור טקסט חדש על ידי לחיצה ימנית על "PlayerSlots" אובייקט -> ממשק משתמש -> טקסט ושנה את שמו ל "SectionTitle"
- שנה את ערכי "SectionTitle" RectTransform ל-"יישור למעלה משמאל" Pivot X: 0 Pivot Y: 0 Pos X: 5 Pos Y: 0 רוחב: 160 גובה: 30)
- שנה את הטקסט "SectionTitle" ל-"Inventory" והגדר את גודל הגופן שלו ל-18, יישור לאמצע שמאלי וצבע ל-(0.2, 0.2, 0.2, 1)
- שכפל את האובייקט "SectionTitle", שנה את הטקסט שלו ל-"Crafting" והעבר אותו מתחת לאובייקט "CraftingSlots", ואז הגדר את אותם ערכי RectTransform כמו ה-"SectionTitle" הקודם.
חריץ יצירה:
חריץ היצירה יורכב מתמונת רקע, תמונת פריט וטקסט ספירה:
- צור תמונה חדשה על ידי לחיצה ימנית על אובייקט קנבס -> ממשק משתמש -> תמונה
- שנה את שם התמונה החדשה ל-"slot_template", הגדר את ערכי ה-RectTransform שלה ל-(פוסט X: 0 Pos Y: 0 רוחב: 40 גובה: 40), ושנו את צבעה ל-(0.32, 0.32, 0.32, 0.8)
- שכפל את "slot_template" ושנה את שמו ל-"Item", הזז אותו בתוך "slot_template" אובייקט, שנה את ממדי ה-RectTransform שלו ל- (רוחב: 30 גובה: 30) ואת הצבע ל- (1, 1, 1, 1)
- צור טקסט חדש על ידי לחיצה ימנית על "slot_template" אובייקט -> ממשק משתמש -> טקסט ושנה את שמו ל "Count"
- שנה את ערכי "Count" RectTransform ל-"יישור ימין למטה" Pivot X: 1 Pivot Y: 0 Pos X: 0 Pos Y: 0 רוחב: 30 גובה: 30)
- הגדר "Count" טקסט למספר אקראי (לדוגמה, 12), סגנון גופן למודגש, גודל גופן ל-14, יישור לתחתית ימין וצבע ל-(1, 1, 1, 1)
- הוסף את רכיב הצל לטקסט "Count" והגדר את צבע אפקט ל-(0, 0, 0, 0.5)
התוצאה הסופית צריכה להיראות כך:
חריץ תוצאות (שישמש ליצירת תוצאות):
- שכפל את האובייקט "slot_template" ושנה את שמו ל "result_slot_template"
- שנה את הרוחב והגובה של "result_slot_template" ל-50
כפתור יצירה וגרפיקה נוספת:
- צור כפתור חדש על ידי לחיצה ימנית על "CraftingSlots" אובייקט -> ממשק משתמש -> כפתור ושנה את שמו ל "CraftButton"
- הגדר את ערכי "CraftButton" RectTransform ל-"יישור שמאל באמצע" Pivot X: 1 Pivot Y: 0.5 Pos X: 0 Pos Y: 0 רוחב: 40 גובה: 40)
- שינוי טקסט של "CraftButton" ל "Craft"
- צור תמונה חדשה על ידי לחיצה ימנית על "CraftingSlots" אובייקט -> ממשק משתמש -> תמונה ושנה את שמה ל "Arrow"
- הגדר את ערכי "Arrow" RectTransform ל-"יישור ימין באמצע" Pivot X: 0 Pivot Y: 0.5 Pos X: 10 Pos Y: 0 רוחב: 30 גובה: 30)
עבור תמונת המקור, אתה יכול להשתמש בתמונה למטה (לחץ לחיצה ימנית -> שמור בשם.. כדי להוריד אותה). לאחר הייבוא הגדר את סוג הטקסטורה שלו ל-"Sprite (2D and UI)" ואת מצב הסינון ל- "Point (no filter)"
- לחץ לחיצה ימנית על "CraftingSlots" -> צור ריק ושנה את שמו ל-"ResultSlot", אובייקט זה יכיל את משבצת התוצאה
- הגדר את ערכי "ResultSlot" RectTransform ל-"יישור ימין באמצע" Pivot X: 0 Pivot Y: 0.5 Pos X: 50 Pos Y: 0 רוחב: 50 גובה: 50)
הגדרת ממשק המשתמש מוכנה.
שלב 2: מערכת יצירה של תוכנית
מערכת יצירה זו תכלול 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" אובייקט עובר ל-"Result Slot Container" משתנה, "CraftButton" אובייקט עובר למשתנה "Craft Button", "slot_template" אובייקט עם סקריפט SC_SlotTemplate מצורף עובר למשתנה "Slot Template" ו-"result_slot_template" אובייקט עם סקריפט SC_SlotTemplate מצורף עובר למשתנה "Result Slot Template"):
כפי שכבר שמתם לב, ישנם שני מערכים ריקים בשם "Player Slots" ו-"Items". "Player Slots" יכיל את מספר המשבצות הזמינות (עם פריט או ריק) ו-"Items" יכיל את כל הפריטים הזמינים יחד עם המתכונים שלהם (אופציונלי).
הגדרת פריטים:
בדוק את הספרייטים למטה (במקרה שלי יהיו לי 5 פריטים):
(סלע)
(יהלום)
(עץ)
(חֶרֶב)
(חרב יהלום)
- הורד כל ספרייט (קליק ימני -> שמור בשם...) וייבא אותם לפרויקט שלך (בהגדרות ייבוא הגדר את סוג הטקסטורה שלו ל-"Sprite (2D and UI)" ואת מצב הסינון ל- "Point (no filter)"
- ב-SC_ItemCrafting שנה את גודל הפריטים ל-5 והקצה כל ספרייט למשתנה 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 פעמים, מופרד בפסיק (,)
דרך קלה לגלות את המתכון היא על ידי לחיצה על Play, לאחר מכן מיקום הפריטים לפי הסדר שברצונך ליצור, ואז לחיצה על "Craft", לאחר מכן, לחץ על (Ctrl + Shift + C) כדי לפתוח את Unity Console ולראות את שורה מודפסת חדשה (ניתן ללחוץ על "Craft" מספר פעמים כדי להדפיס מחדש את השורה), השורה המודפסת היא מתכון היצירה.
לדוגמה, השילוב שלהלן מתאים למתכון הזה: רוק,,רוק,,רוק,,רוק,,עץ (הערה: זה עשוי להיות שונה עבורך אם לשפריטים שלך יש שמות שונים).
נשתמש במתכון שלמעלה כדי ליצור חרב.
- העתק את השורה המודפסת, ובמערך "Items" הדבק אותה במשתנה "Craft Recipe" מתחת לפריט "sword":
עכשיו כשאתה חוזר על אותו שילוב אתה אמור להיות מסוגל ליצור חרב.
מתכון לחרב יהלום זהה, אבל במקום סלע זה יהלום:
מערכת היצירה מוכנה כעת.