tutoriales.com

Creando un Sistema de Crafteo y Recetas en Godot 4: Fabricando el Mundo de tu Juego

Este tutorial te guiará paso a paso en la creación de un sistema de crafteo completo para tus juegos en Godot 4. Aprenderás a definir recetas, gestionar inventarios y procesar la fabricación de objetos. Es esencial para añadir profundidad y rejugabilidad a tus proyectos.

Intermedio15 min de lectura4 views
Reportar error

🚀 Introducción al Sistema de Crafteo en Godot 4

El crafteo, o fabricación de objetos, es una mecánica fundamental en muchos géneros de videojuegos, desde RPGs y supervivencia hasta simulación. Permite a los jugadores combinar materiales básicos para crear objetos más complejos, herramientas, armas o consumibles, añadiendo una capa de estrategia y progresión. Implementar un sistema de crafteo robusto en Godot 4 puede parecer una tarea desafiante, pero con una buena planificación y el enfoque correcto, es totalmente manejable.

En este tutorial, construiremos un sistema de crafteo que incluye:

  • Definición de ítems y sus propiedades.
  • Estructura para recetas de crafteo.
  • Una interfaz de usuario (UI) básica para la mesa de crafteo.
  • Lógica para verificar los ingredientes del inventario.
  • Proceso de fabricación y gestión del inventario.

¿Por qué es importante un buen sistema de crafteo?

Un sistema de crafteo bien diseñado no solo añade horas de juego, sino que también fomenta la exploración, la recolección de recursos y la toma de decisiones por parte del jugador. Transforma objetos simples en recursos valiosos y crea un ciclo de juego gratificante.

💡 Consejo: Piensa en cómo el crafteo encajará con otras mecánicas de tu juego, como el inventario, el combate o la progresión del personaje.

🛠️ Configuración Inicial: Items y Estructura de Datos

Antes de sumergirnos en la lógica del crafteo, necesitamos definir cómo representaremos nuestros ítems y nuestras recetas. Utilizaremos el concepto de Resource en Godot para esto, lo que nos permitirá crear datos fácilmente reusables y editables directamente desde el editor.

1. Creando la Clase Base ItemResource

Crearemos un nuevo script que herede de Resource. Este será el esqueleto para todos los ítems de nuestro juego. Nos permitirá definir propiedades comunes como nombre, descripción, icono y una cantidad.

Crea un nuevo script llamado ItemResource.gd en una carpeta res://Resources/Items/ (o similar).

# ItemResource.gd
extends Resource
class_name ItemResource

@export var id: String = ""
@export var item_name: String = "Nuevo Item"
@export_multiline var description: String = "Una descripción genérica de este item."
@export var texture: Texture2D
@export var stackable: bool = true
@export var max_stack_size: int = 99

func _to_string():
	return "ItemResource(%s)" % item_name

Explicación:

  • class_name ItemResource: Permite que este recurso sea reconocido por su nombre de clase en Godot y facilita la creación de nuevos recursos de este tipo.
  • @export: Hace que estas propiedades sean editables en el Inspector cuando creamos una nueva instancia de ItemResource.
  • id: Un identificador único para el ítem, útil para la lógica del juego.
  • item_name, description, texture: Propiedades obvias para la presentación.
  • stackable, max_stack_size: Esencial para la gestión del inventario, define si el ítem se puede apilar y cuántos pueden caber en un solo slot.

2. Creando Recetas: RecipeResource

Ahora necesitamos una forma de definir qué ingredientes se necesitan para fabricar qué objeto. Creamos otro Resource para nuestras recetas.

Crea un nuevo script llamado RecipeResource.gd en res://Resources/Recipes/.

# RecipeResource.gd
extends Resource
class_name RecipeResource

@export var recipe_name: String = "Nueva Receta"
@export var result_item: ItemResource
@export var result_quantity: int = 1
@export var ingredients: Dictionary = {}

func _to_string():
	return "RecipeResource(%s)" % recipe_name

func has_ingredients(inventory: Dictionary) -> bool:
	for item_id in ingredients:
		if not inventory.has(item_id) or inventory[item_id] < ingredients[item_id]:
			return false
	return true

func get_ingredient_quantity(item_id: String) -> int:
	return ingredients.get(item_id, 0)

Explicación:

  • result_item: Una referencia al ItemResource que se producirá.
  • result_quantity: Cuántas unidades del result_item se producen.
  • ingredients: Un diccionario donde la clave es el id del ItemResource requerido y el valor es la cantidad necesaria. Por ejemplo: {"madera": 2, "piedra": 1}.
  • has_ingredients(inventory): Una función útil para verificar si el jugador tiene todos los ingredientes necesarios en su inventario. Asumimos que inventory es un diccionario con id del ítem como clave y cantidad como valor.
🔥 Importante: La consistencia en el uso de los `id` de los ítems es crucial. Deben ser únicos y usarse tanto en `ItemResource` como en `RecipeResource` y en el inventario.

📦 Gestión del Inventario (Simplificado)

Para que nuestro sistema de crafteo funcione, necesitamos una forma de almacenar los ítems del jugador. Para este tutorial, usaremos una implementación simple de inventario como un diccionario global o en un Singleton. En un juego real, esto podría ser mucho más complejo (con slots limitados, etc.), pero para el crafteo, solo necesitamos saber qué ítems y cuántos tiene el jugador.

Crea un script Singleton llamado InventoryManager.gd (Proyecto -> Configuración del Proyecto -> Autocarga, añade el script como InventoryManager).

# InventoryManager.gd (Singleton)
extends Node

var player_inventory: Dictionary = {}

func add_item(item_resource: ItemResource, quantity: int = 1):
	if not item_resource:
		return
	
	var item_id = item_resource.id
	
	if player_inventory.has(item_id):
		player_inventory[item_id] += quantity
	else:
		player_inventory[item_id] = quantity
	
	print("Añadido %d %s. Inventario: %s" % [quantity, item_resource.item_name, player_inventory])

func remove_item(item_resource: ItemResource, quantity: int = 1) -> bool:
	if not item_resource:
		return false
	
	var item_id = item_resource.id
	
	if player_inventory.has(item_id) and player_inventory[item_id] >= quantity:
		player_inventory[item_id] -= quantity
		if player_inventory[item_id] <= 0:
			player_inventory.erase(item_id)
		print("Removido %d %s. Inventario: %s" % [quantity, item_resource.item_name, player_inventory])
		return true
	
	print("No se pudo remover %d %s. Inventario: %s" % [quantity, item_resource.item_name, player_inventory])
	return false

func get_item_quantity(item_id: String) -> int:
	return player_inventory.get(item_id, 0)

func get_inventory() -> Dictionary:
	return player_inventory

Este InventoryManager nos permitirá añadir y remover ítems de forma centralizada y verificar las cantidades disponibles. Esencialmente, player_inventory es un diccionario donde las claves son los id de los ítems y los valores son las cantidades.


🎨 Interfaz de Usuario (UI) para la Mesa de Crafteo

Necesitamos una forma visual para que el jugador interactúe con el sistema de crafteo. Crearemos una interfaz simple con un Control principal, una lista de recetas y un panel de detalles.

1. Diseño de la Escena UI

Crea una nueva escena de tipo Control y guárdala como CraftingUI.tscn.

Estructura de Nodos:

  • CraftingUI (Control)
    • PanelContainer (Contiene toda la UI)
      • HBoxContainer
        • VBoxContainer (Para la lista de recetas)
          • Label (Recetas disponibles)
          • ItemList (Aquí se mostrarán las recetas)
        • VBoxContainer (Para detalles de la receta seleccionada y botón de crafteo)
          • Label (Nombre de la Receta)
          • TextureRect (Icono del resultado)
          • Label (Descripción del resultado)
          • VBoxContainer (Panel de ingredientes)
            • Label (Ingredientes)
            • GridContainer (Para mostrar cada ingrediente)
          • Button (Craftear)

Configuración de Nodos clave:

  • ItemList: Establece Select Mode a Single. Conecta la señal item_selected a tu script CraftingUI.gd.
  • Button: Conecta la señal pressed a tu script.
  • Ajusta los tamaños y anclajes (Ctrl + Arrastrar en el editor 2D) para que la UI se vea bien en diferentes resoluciones.
CraftingUI (Control) PanelContainer HBoxContainer VBoxContainer (Recetas) VBoxContainer (Detalles) ItemList Label TextureRect GridContainer Button Estructura jerárquica de la interfaz de usuario

2. Script para la CraftingUI

Crea un script llamado CraftingUI.gd para el nodo CraftingUI.

# CraftingUI.gd
extends Control

@onready var recipe_list: ItemList = $PanelContainer/HBoxContainer/VBoxContainer/ItemList
@onready var recipe_name_label: Label = $PanelContainer/HBoxContainer/VBoxContainer2/Label
@onready var result_icon: TextureRect = $PanelContainer/HBoxContainer/VBoxContainer2/TextureRect
@onready var result_description_label: Label = $PanelContainer/HBoxContainer/VBoxContainer2/Label2
@onready var ingredients_grid: GridContainer = $PanelContainer/HBoxContainer/VBoxContainer2/VBoxContainer/GridContainer
@onready var craft_button: Button = $PanelContainer/HBoxContainer/VBoxContainer2/Button

var available_recipes: Array[RecipeResource] = []
var selected_recipe: RecipeResource = null

func _ready():
	hide()
	# Carga algunas recetas de ejemplo
	load_all_recipes()
	update_recipe_list()
	_on_item_selected(0) # Selecciona la primera receta por defecto

func load_all_recipes():
	# En un juego real, cargarías estas recetas desde una carpeta o un archivo JSON
	# Por simplicidad, las creamos en el editor y las cargamos aquí.
	# Asegúrate de haber creado tus ItemResources y RecipeResources en el editor.
	
	# Ejemplo: Crear ItemResources
	var wood = load("res://Resources/Items/Wood.tres") as ItemResource
	var stone = load("res://Resources/Items/Stone.tres") as ItemResource
	var axe = load("res://Resources/Items/Axe.tres") as ItemResource
	
	# Ejemplo: Crear RecipeResources (en el editor)
	# Receta 1: Hacha (2 madera, 1 piedra)
	var axe_recipe = RecipeResource.new()
	axe_recipe.recipe_name = "Hacha de Piedra"
	axe_recipe.result_item = axe
	axe_recipe.result_quantity = 1
	axe_recipe.ingredients = {wood.id: 2, stone.id: 1}
	available_recipes.append(axe_recipe)
	
	# Puedes cargar más recetas aquí desde archivos .tres que crees en el editor
	# Ejemplo: var another_recipe = load("res://Resources/Recipes/AnotherRecipe.tres") as RecipeResource
	# available_recipes.append(another_recipe)

func update_recipe_list():
	recipe_list.clear()
	for recipe in available_recipes:
		recipe_list.add_item(recipe.recipe_name, recipe.result_item.texture if recipe.result_item else null)

func _on_item_selected(index: int):
	if index < 0 or index >= available_recipes.size():
		selected_recipe = null
		clear_details()
		return
	
	selected_recipe = available_recipes[index]
	update_recipe_details()

func update_recipe_details():
	if not selected_recipe:
		clear_details()
		return
	
	recipe_name_label.text = selected_recipe.recipe_name
	if selected_recipe.result_item:
		result_icon.texture = selected_recipe.result_item.texture
		result_description_label.text = "Resultado: %s (x%d)\n%s" % [selected_recipe.result_item.item_name, selected_recipe.result_quantity, selected_recipe.result_item.description]
	else:
		result_icon.texture = null
		result_description_label.text = "Resultado desconocido"
	
	# Limpiar ingredientes anteriores
	for child in ingredients_grid.get_children():
		child.queue_free()
	
	# Mostrar ingredientes requeridos
	for item_id in selected_recipe.ingredients:
		var required_quantity = selected_recipe.ingredients[item_id]
		var current_quantity = InventoryManager.get_item_quantity(item_id)
		
		var label = Label.new()
		label.text = "- %s: %d/%d" % [item_id, current_quantity, required_quantity]
		if current_quantity >= required_quantity:
			label.add_theme_color_override("font_color", Color.GREEN)
		else:
			label.add_theme_color_override("font_color", Color.RED)
		ingredients_grid.add_child(label)
	
	craft_button.disabled = not selected_recipe.has_ingredients(InventoryManager.get_inventory())

func clear_details():
	recipe_name_label.text = ""
	result_icon.texture = null
	result_description_label.text = "Selecciona una receta para ver los detalles."
	for child in ingredients_grid.get_children():
		child.queue_free()
	craft_button.disabled = true

func _on_craft_button_pressed():
	if not selected_recipe:
		return
	
	# Verificar una última vez si tenemos los ingredientes
	if selected_recipe.has_ingredients(InventoryManager.get_inventory()):
		# Remover ingredientes
		for item_id in selected_recipe.ingredients:
			InventoryManager.remove_item(load("res://Resources/Items/%s.tres" % item_id.capitalize()) as ItemResource, selected_recipe.ingredients[item_id])
		
		# Añadir el resultado
		InventoryManager.add_item(selected_recipe.result_item, selected_recipe.result_quantity)
		
		print("¡Crafteado %s (x%d)!" % [selected_recipe.result_item.item_name, selected_recipe.result_quantity])
		
		# Actualizar la UI después del crafteo
		update_recipe_details()
	else:
		print("No tienes los ingredientes necesarios para craftear %s." % selected_recipe.recipe_name)

func toggle_visibility():
	visible = not visible
	if visible:
		update_recipe_list()
		_on_item_selected(recipe_list.get_selected_items()[0] if not recipe_list.get_selected_items().is_empty() else 0)
		get_tree().paused = true # Pausar el juego si la UI está abierta
	else:
		get_tree().paused = false

📌 Nota: En el ejemplo `load_all_recipes()`, creamos las recetas directamente en el código para simplificar. En un juego real, deberías pre-crear tus `ItemResource` y `RecipeResource` como archivos `.tres` en el editor de Godot y luego cargarlos usando `load()` o `ResourceLoader.load()`. Esto hace tu juego más modular y fácil de balancear.

3. Crear ItemResource y RecipeResource en el editor

Para que el ejemplo funcione, necesitas crear algunos archivos ItemResource y RecipeResource en el editor:

  1. En el FileSystem, clic derecho en la carpeta res://Resources/Items/ -> Nuevo Recurso... -> Busca ItemResource. Nombra uno Wood.tres (Madera), otro Stone.tres (Piedra) y otro Axe.tres (Hacha).
  2. Edita las propiedades de cada uno en el Inspector: asigna un id (e.g., "madera", "piedra", "hacha"), nombre, descripción y una Texture2D si tienes iconos.
  3. Crea un RecipeResource en res://Resources/Recipes/ (e.g., AxeRecipe.tres).
  4. En el Inspector, edita AxeRecipe.tres:
    • recipe_name: "Hacha de Piedra"
    • result_item: Arrastra res://Resources/Items/Axe.tres aquí.
    • result_quantity: 1
    • ingredients: Haz clic en el diccionario, añade 2 entradas:
      • Clave: madera (string), Valor: 2 (int)
      • Clave: piedra (string), Valor: 1 (int)

🎮 Integrando la UI y la Lógica del Juego

Ahora que tenemos la UI y la lógica de datos, necesitamos unirlas con nuestro juego. Asumiremos que tienes un nodo principal de juego (e.g., Game.tscn o Main.tscn).

1. Añadiendo la CraftingUI a la escena principal

En tu escena principal del juego:

  1. Instancia CraftingUI.tscn como hijo de tu nodo principal de juego (o de un CanvasLayer para que siempre se renderice por encima).
  2. En el script de tu jugador o tu nodo principal, añade una forma de abrir/cerrar la UI de crafteo. Por ejemplo, al presionar una tecla.
# En el script de tu juego principal (e.g., Game.gd)
extends Node2D # O whatever your main scene is

@onready var crafting_ui = $CraftingUI # Asegúrate de que la ruta sea correcta

func _ready():
	crafting_ui.hide()

func _input(event):
	if event.is_action_pressed("toggle_crafting_ui"):
		crafting_ui.toggle_visibility()

Asegúrate de definir la acción toggle_crafting_ui en Proyecto -> Configuración del Proyecto -> Mapa de Entrada (e.g., con la tecla C).

2. Probando el Sistema

Para probar, puedes añadir algunos ítems al inventario del jugador al inicio del juego:

# En el script de tu juego principal (e.g., Game.gd), dentro de _ready()
func _ready():
	crafting_ui.hide()
	# Añadir algunos ítems al inventario para probar
	var wood = load("res://Resources/Items/Wood.tres") as ItemResource
	var stone = load("res://Resources/Items/Stone.tres") as ItemResource
	
	InventoryManager.add_item(wood, 5)
	InventoryManager.add_item(stone, 3)
	
	print("Inventario inicial: ", InventoryManager.get_inventory())

Ahora, ejecuta el juego. Deberías poder:

  1. Abrir la UI de crafteo con la tecla asignada.
  2. Ver las recetas disponibles.
  3. Seleccionar una receta y ver sus detalles e ingredientes necesarios (con colores indicando si los tienes).
  4. Si tienes los ingredientes, el botón Craftear estará habilitado.
  5. Al hacer clic en Craftear, los ingredientes deberían desaparecer de tu inventario y el ítem resultante debería aparecer.
⚠️ Advertencia: Asegúrate de que los `id` de tus `ItemResource` (e.g., "madera") coincidan exactamente con las claves de los diccionarios de `ingredients` en tus `RecipeResource`. Un error tipográfico impedirá que el sistema funcione.

✨ Mejoras y Posibles Extensiones

Este es un sistema de crafteo funcional, pero hay muchas formas de expandirlo y mejorarlo para tu juego:

  • Sistema de slots de inventario: En lugar de un diccionario simple, usa una lista de slots para un inventario con espacio limitado.
  • Crafteo por lotes: Permitir al jugador craftear múltiples unidades a la vez (e.g., mantener presionado el botón).
  • Categorías de recetas: Agrupar recetas por tipo (armas, herramientas, consumibles) para una mejor navegación en la UI.
  • Requisitos de estación de crafteo: Algunas recetas solo se pueden craftear en una Mesa de Trabajo o una Forja.
  • Desbloqueo de recetas: Que las recetas se descubran a medida que el jugador avanza, recoge ítems o sube de nivel.
  • Animaciones y efectos de sonido: Añadir feedback visual y auditivo al craftear.
  • Persistencia de datos: Guardar y cargar el inventario del jugador y las recetas descubiertas.
90% Completado (Funcional)

❓ Preguntas Frecuentes (FAQ)

¿Cómo hago que el juego cargue las recetas de forma dinámica? Puedes guardar todas tus `RecipeResource` en una carpeta (e.g., `res://Resources/Recipes/`) y luego usar `DirAccess` para iterar sobre los archivos en esa carpeta y cargarlos dinámicamente. Esto es útil para cuando tienes muchas recetas.
# Ejemplo de carga dinámica de recetas
func load_recipes_dynamically(path: String) -> Array[RecipeResource]:
    var loaded_recipes: Array[RecipeResource] = []
    var dir = DirAccess.open(path)
    if dir:
        dir.list_dir_begin()
        var file_name = dir.get_next()
        while file_name != "":
            if file_name.ends_with(".tres"):
                var recipe = load(path + file_name) as RecipeResource
                if recipe:
                    loaded_recipes.append(recipe)
            file_name = dir.get_next()
        dir.list_dir_end()
    return loaded_recipes

# En _ready() de CraftingUI.gd:
# available_recipes = load_recipes_dynamically("res://Resources/Recipes/")
¿Cómo puedo manejar ítems no apilables? El `ItemResource` ya tiene una propiedad `stackable`. Si un ítem no es apilable, su `max_stack_size` podría ser `1`. Para el inventario, en lugar de solo contar cantidades, necesitarías almacenar referencias a los ítems individuales en *slots*. Esto hace el inventario más complejo, pero más realista para ítems únicos como espadas legendarias.
¿Puedo usar datos JSON para definir ítems y recetas? ¡Sí! Usar JSON es una excelente manera de gestionar datos externos. Puedes cargar archivos JSON que contengan arrays de ítems y recetas, parsearlos y luego crear instancias de tus `ItemResource` y `RecipeResource` en tiempo de ejecución. Esto permite a los diseñadores de juegos ajustar valores sin tocar código Godot. Necesitarías una función que tome un diccionario JSON y construya el `ItemResource` o `RecipeResource` correspondiente.

✅ Conclusión

Has construido un sistema de crafteo básico pero funcional en Godot 4. Has aprendido a estructurar tus datos con Resource, a gestionar un inventario simple y a crear una interfaz de usuario interactiva. Este es un punto de partida sólido desde el cual puedes expandir y personalizar tu sistema de crafteo para que se ajuste perfectamente a las necesidades de tu juego.

¡Experimenta, refina y diviértete creando experiencias de crafteo memorables para tus jugadores!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!