Creación de un sistema de elaboración de artículos e inventario en Unity
En este tutorial, mostraré cómo crear un sistema de creación de objetos e inventario estilo Minecraft en Unity.
La creación de elementos en los videojuegos es un proceso de combinación de elementos específicos (generalmente más simples) en elementos más complejos, con propiedades nuevas y mejoradas. Por ejemplo, combinar madera y piedra para formar un pico, o combinar láminas de metal y madera para formar una espada.
El sistema de elaboración a continuación es compatible con dispositivos móviles y completamente automatizado, lo que significa que funcionará con cualquier diseño de interfaz de usuario y con la capacidad de crear recetas de elaboración personalizadas.
Paso 1: configurar la interfaz de usuario de creación
Comenzamos configurando la interfaz de usuario de creación:
- Cree un nuevo lienzo (Unity barra de tareas superior: GameObject -> UI -> Canvas)
- Cree una nueva imagen haciendo clic derecho en el objeto Canvas -> UI -> Imagen
- Cambie el nombre del objeto de imagen a "CraftingPanel" y cambie su imagen de origen a la predeterminada "UISprite"
- Cambie los valores de "CraftingPanel" RectTransform a (Pos X: 0 Pos Y: 0 Ancho: 410 Alto: 365)
- Cree dos objetos dentro de "CraftingPanel" (haga clic derecho en CraftingPanel -> Crear vacío, 2 veces)
- Cambie el nombre del primer objeto a "CraftingSlots" y cambie sus valores RectTransform a ("Alineación superior izquierda" Pivote X: 0 Pivote Y: 1 Pos X: 50 Pos Y: -35 Ancho: 140 Alto: 140). Este objeto contendrá espacios de elaboración.
- Cambie el nombre del segundo objeto a "PlayerSlots" y cambie sus valores de RectTransform a ("Estiramiento superior horizontal" Pivote X: 0.5 Pivote Y: 1 Izquierda: 0 Pos Y: -222 Derecha: 0 Altura: 100). Este Objeto contendrá espacios para jugadores.
Sección de título:
- Cree un nuevo texto haciendo clic derecho en "PlayerSlots" Objeto -> UI -> Texto y cámbiele el nombre a "SectionTitle"
- Cambie los valores de "SectionTitle" RectTransform a ("Alineación superior izquierda" Pivote X: 0 Pivote Y: 0 Pos X: 5 Pos Y: 0 Ancho: 160 Alto: 30)
- Cambie el texto "SectionTitle" a "Inventory" y establezca su Tamaño de fuente en 18, Alineación en Medio izquierdo y Color en (0,2, 0,2, 0,2, 1)
- Duplique el objeto "SectionTitle", cambie su texto a "Crafting" y muévalo debajo del objeto "CraftingSlots", luego establezca los mismos valores de RectTransform que el "SectionTitle" anterior.
Ranura de elaboración:
El espacio de creación constará de una imagen de fondo, una imagen del elemento y un texto de recuento:
- Cree una nueva imagen haciendo clic derecho en el objeto Canvas -> UI -> Imagen
- Cambie el nombre de la nueva imagen a "slot_template", establezca sus valores RectTransform en (Post X: 0 Pos Y: 0 Ancho: 40 Alto: 40) y cambie su color a (0,32, 0,32, 0,32, 0,8)
- Duplique "slot_template" y cámbiele el nombre a "Item", muévalo dentro del Objeto "slot_template", cambie sus dimensiones RectTransform a (Ancho: 30 Alto: 30) y Color a (1, 1, 1, 1)
- Cree un nuevo texto haciendo clic derecho en "slot_template" Objeto -> UI -> Texto y cámbiele el nombre a "Count"
- Cambie los valores de "Count" RectTransform a ("Alineación inferior derecha" Pivote X: 1 Pivote Y: 0 Pos X: 0 Pos Y: 0 Ancho: 30 Alto: 30)
- Establezca el texto "Count" en un número aleatorio (ej. 12), el estilo de fuente en negrita, el tamaño de fuente en 14, la alineación en la parte inferior derecha y el color en (1, 1, 1, 1).
- Agregue el componente Sombra al texto "Count" y establezca el Color del efecto en (0, 0, 0, 0,5)
El resultado final debería verse así:
Ranura de resultados (que se utilizará para crear resultados):
- Duplique el objeto "slot_template" y cámbiele el nombre a "result_slot_template"
- Cambie el ancho y alto de "result_slot_template" a 50
Botón de elaboración y gráficos adicionales:
- Cree un nuevo botón haciendo clic derecho en "CraftingSlots" Objeto -> UI -> Botón y cámbiele el nombre a "CraftButton"
- Establezca los valores de "CraftButton" RectTransform en ("Alineación media izquierda" Pivote X: 1 Pivote Y: 0,5 Pos X: 0 Pos Y: 0 Ancho: 40 Alto: 40)
- Cambie el texto de "CraftButton" a "Craft"
- Cree una nueva imagen haciendo clic derecho en "CraftingSlots" Objeto -> UI -> Imagen y cámbiele el nombre a "Arrow"
- Establezca los valores de "Arrow" RectTransform en ("Alineación media derecha" Pivote X: 0 Pivote Y: 0,5 Pos X: 10 Pos Y: 0 Ancho: 30 Alto: 30)
Para la imagen de origen, puede usar la imagen a continuación (haga clic derecho -> Guardar como... para descargarla). Después de importar, establezca su tipo de textura en "Sprite (2D and UI)" y el modo de filtro en "Point (no filter)"
- Haga clic derecho en "CraftingSlots" -> Crear vacío y cámbiele el nombre a "ResultSlot", este objeto contendrá el espacio resultante
- Establezca los valores de "ResultSlot" RectTransform en ("Alineación media derecha" Pivote X: 0 Pivote Y: 0,5 Pos X: 50 Pos Y: 0 Ancho: 50 Alto: 50)
La configuración de la interfaz de usuario está lista.
Paso 2: Sistema de elaboración del programa
Este sistema de elaboración constará de 2 scripts, SC_ItemCrafting.cs y SC_SlotTemplate.cs
- Cree un nuevo script, asígnele el nombre "SC_ItemCrafting" y luego pegue el siguiente código dentro de él:
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);
}
}
}
}
- Cree un nuevo script, asígnele el nombre "SC_SlotTemplate" y luego pegue el siguiente código dentro de él:
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();
}
}
Preparando plantillas de tragamonedas:
- Adjunte el script SC_SlotTemplate al objeto "slot_template" y asigne sus variables (el componente de imagen en el mismo objeto va a la variable "Container", la imagen secundaria "Item" va a la variable "Item" y una variable secundaria "Count" El texto va a la variable "Count")
- Repita el mismo proceso para el objeto "result_slot_template" (adjunte el script SC_SlotTemplate y asigne variables de la misma manera).
Preparando el sistema artesanal:
- Adjunte el script SC_ItemCrafting al objeto Canvas y asigne sus variables (el objeto "PlayerSlots" va a la variable "Player Slots Container", el objeto "CraftingSlots" va a la variable "Crafting Slots Container", el objeto "ResultSlot" va a "Result Slot Container" variable, el objeto "CraftButton" va a la variable "Craft Button", el objeto "slot_template" con el script SC_SlotTemplate adjunto va a la variable "Slot Template" y el objeto "result_slot_template" con el script SC_SlotTemplate adjunto va a la variable "Result Slot Template"):
Como ya habrás notado, hay dos matrices vacías llamadas "Player Slots" y "Items". "Player Slots" contendrá la cantidad de espacios disponibles (con artículo o vacíos) y "Items" contendrá todos los artículos disponibles junto con sus recetas (opcional).
Configuración de elementos:
Mira los sprites a continuación (en mi caso tendré 5 elementos):
(roca)
(diamante)
(madera)
(espada)
(espada de diamante)
- Descargue cada sprite (Clic derecho -> Guardar como...) e impórtelos a su proyecto (en Configuración de importación establezca su Tipo de textura en "Sprite (2D and UI)" y Modo de filtro en "Point (no filter)"
- En SC_ItemCrafting cambie el Tamaño de los elementos a 5 y asigne cada sprite a la variable Item Sprite.
"Stackable" La variable controla si los elementos se pueden apilar juntos en una ranura (por ejemplo, es posible que desee permitir el apilamiento solo de materiales simples como roca, diamante y madera).
"Craft Recipe" La variable controla si este elemento se puede crear (vacío significa que no se puede crear).
- Para "Player Slots" establezca el Tamaño de matriz en 27 (mejor ajuste para el Panel de elaboración actual, pero puede establecer cualquier número).
Cuando presionas Reproducir, notarás que las ranuras se inicializan correctamente, pero no hay elementos:
Para agregar un elemento a cada ranura necesitaremos asignar un Sprite de elemento a la variable "Item Sprite" y establecer "Item Count" en cualquier número positivo (todo lo que esté debajo de 1 y/o los elementos no apilables se interpretarán como 1):
- Asigne el sprite "rock" al Elemento 0 / "Item Count" 14, el sprite "wood" al Elemento 1 / "Item Count" 8, el sprite "diamond" al Elemento 2 / "Item Count" 8 (Asegúrese de que los sprites sean los mismos que en "Items" Array, de lo contrario no funcionará).
Los elementos ahora deberían aparecer en las ranuras del jugador. Puede cambiar su posición haciendo clic en el elemento y luego haciendo clic en la ranura a la que desea moverlo.
Recetas de elaboración:
La elaboración de recetas le permite crear un elemento combinando otros elementos en un orden específico:
El formato para elaborar la receta es el siguiente: [item_sprite_name]([item count])*opcional... repetido 9 veces, separado por coma (,)
Una manera fácil de descubrir la receta es presionando Reproducir, luego colocando los elementos en el orden que deseas crear, luego presionando "Craft", luego de eso, presiona (Ctrl + Shift + C) para abrir la Consola Unity y ver la línea recién impresa (puede hacer clic en "Craft" varias veces para volver a imprimir la línea), la línea impresa es la receta de elaboración.
Por ejemplo, la siguiente combinación corresponde a esta receta: roca,,roca,,roca,,roca,,madera (NOTA: puede ser diferente para ti si tus sprites tienen nombres diferentes).
Usaremos la receta anterior para fabricar una espada.
- Copie la línea impresa y, en la matriz "Items", péguela en la variable "Craft Recipe" debajo del elemento "sword":
Ahora, al repetir esa misma combinación, deberías poder fabricar una espada.
La receta para una espada de diamante es la misma, pero en lugar de roca es diamante:
El sistema de elaboración ya está listo.