tutoriales.com

Creando un Sistema de Inventario Básico en Godot 4: Gestión de Objetos y Mochila

Este tutorial te guiará a través de los pasos esenciales para construir un sistema de inventario funcional en Godot 4. Aprenderás a definir objetos, gestionar slots de inventario y crear una interfaz de usuario interactiva para tu mochila de jugador.

Intermedio20 min de lectura38 views12 de marzo de 2026Reportar error

Sistema de Inventario Básico en Godot 4: Gestión de Objetos y Mochila 🎒

¡Hola, desarrollador de videojuegos! ¿Estás listo para darle a tus jugadores la capacidad de recolectar, usar y gestionar objetos en tu juego de Godot 4? Un sistema de inventario es fundamental en casi cualquier RPG, juego de aventura o survival. En este tutorial, construiremos un sistema de inventario básico pero robusto desde cero.

Aquí, nos enfocaremos en la lógica detrás de los objetos, los slots de inventario y cómo la interfaz de usuario interactúa con estos componentes. ¡Prepárate para organizar tus píxeles!

💡 Consejo: Familiarizarte con GDScript básico y el editor de Godot 4 te ayudará mucho a seguir este tutorial.

🎯 ¿Qué aprenderemos en este tutorial?

  • Definir la estructura de nuestros objetos con un recurso personalizado (Resource).
  • Crear un sistema de Inventario que gestione los slots.
  • Implementar una interfaz de usuario para la mochila del jugador (UI).
  • Añadir y eliminar objetos del inventario.
  • Visualizar los objetos en la interfaz de usuario.

📖 Paso 1: Definición de un Objeto (ItemResource) ✨

El primer paso es definir qué es un objeto en nuestro juego. En Godot, la mejor manera de hacer esto es usando un Resource. Esto nos permite crear plantillas de objetos que pueden ser instanciadas y utilizadas en cualquier lugar del juego sin necesidad de tener un nodo en la escena.

Creando ItemResource.gd

Crea un nuevo script llamado ItemResource.gd en una carpeta res/items (o donde prefieras) y pégalo:

# res/items/ItemResource.gd
class_name ItemResource extends Resource

@export_category("Item Data")
@export var id: String = "new_item" # Identificador único del item
@export var item_name: String = "Nuevo Objeto" # Nombre visible
@export var description: String = "Una descripción genial para este objeto." # Descripción del item
@export var texture: Texture2D # Textura del icono
@export var max_stack_size: int = 1 # Cuántos items se pueden apilar en un slot
@export var is_stackable: bool = false # Si el item se puede apilar o no

func _init(p_id: String = "new_item", p_name: String = "Nuevo Objeto", p_desc: String = "", p_texture: Texture2D = null, p_max_stack_size: int = 1, p_stackable: bool = false):
    id = p_id
    item_name = p_name
    description = p_desc
    texture = p_texture
    max_stack_size = p_max_stack_size
    is_stackable = p_stackable

func can_stack_with(other_item_resource: ItemResource) -> bool:
    return is_stackable and other_item_resource.is_stackable and id == other_item_resource.id
📌 Nota: Usamos `@export` para que estas propiedades sean editables directamente en el inspector de Godot cuando creamos un nuevo recurso de `ItemResource`. `class_name` permite que Godot reconozca este tipo de recurso por su nombre.

Creando Recursos de Objetos (Ejemplos)

Ahora, puedes crear algunos objetos de ejemplo. Haz clic derecho en la carpeta res/items (o la que uses) en el Filesystem, selecciona New Resource... y busca ItemResource. Crea al menos dos:

  1. res/items/Potion.tres:

    • id: potion_health
    • item_name: Poción de Salud
    • description: Restaura un poco de vida.
    • texture: Asigna una textura (puedes crear una simple con un círculo rojo).
    • max_stack_size: 5
    • is_stackable: ✅
  2. res/items/Sword.tres:

    • id: rusty_sword
    • item_name: Espada Oxidada
    • description: Una vieja espada, no muy afilada.
    • texture: Asigna una textura (un icono de espada).
    • max_stack_size: 1
    • is_stackable: ❌

📖 Paso 2: El Slot del Inventario (InventorySlot.gd) 📦

Cada espacio en nuestro inventario será un InventorySlot. Este recurso contendrá una referencia al ItemResource y la cantidad de ese objeto que hay en el slot.

Creando InventorySlot.gd

Crea un script InventorySlot.gd en res/inventory:

# res/inventory/InventorySlot.gd
class_name InventorySlot extends Resource

@export var item_resource: ItemResource = null
@export var current_amount: int = 0

func is_empty() -> bool:
    return item_resource == null or current_amount <= 0

func add_item(item: ItemResource, amount: int = 1) -> int:
    if item_resource == null:
        item_resource = item
        current_amount = amount
        return 0 # Todos los items se añadieron

    if item_resource.can_stack_with(item):
        var space_left = item_resource.max_stack_size - current_amount
        var to_add = min(amount, space_left)
        current_amount += to_add
        return amount - to_add # Devuelve la cantidad restante que no se pudo añadir
    return amount # No se pudo añadir nada si no es stackable o no coincide

func remove_item(amount: int = 1) -> ItemResource:
    if is_empty():
        return null

    var removed_item = item_resource
    current_amount -= amount

    if current_amount <= 0:
        clear_slot()
        return removed_item # Se vació el slot y se devuelve el tipo de item

    return removed_item # Se removieron items, pero el slot no se vació

func clear_slot():
    item_resource = null
    current_amount = 0

func get_item_id() -> String:
    return item_resource.id if item_resource else ""

func get_item_name() -> String:
    return item_resource.item_name if item_resource else "Vacío"

📖 Paso 3: El Sistema de Inventario (Inventory.gd) 👜

Ahora crearemos el corazón de nuestro sistema: el Inventory propiamente dicho. Este script gestionará una colección de InventorySlots y proveerá la lógica para añadir, quitar y buscar objetos.

Creando Inventory.gd

Crea Inventory.gd en res/inventory:

# res/inventory/Inventory.gd
class_name Inventory extends Resource

@export var inventory_size: int = 10 # Número de slots en el inventario
@export var slots: Array[InventorySlot] # Array de slots

signal inventory_changed # Se emite cuando el inventario cambia

func _init(size: int = 10):
    inventory_size = size
    _initialize_slots()

func _initialize_slots():
    slots.clear()
    for i in range(inventory_size):
        slots.append(InventorySlot.new())
    emit_signal("inventory_changed")

func add_item(item: ItemResource, amount: int = 1) -> int:
    var remaining_amount = amount

    # Intenta añadir a slots existentes (stackable)
    for slot in slots:
        if slot.item_resource != null and slot.item_resource.can_stack_with(item):
            remaining_amount = slot.add_item(item, remaining_amount)
            if remaining_amount == 0:
                emit_signal("inventory_changed")
                return 0
    
    # Si quedan items, intenta añadir a slots vacíos
    for slot in slots:
        if slot.is_empty():
            remaining_amount = slot.add_item(item, remaining_amount)
            if remaining_amount == 0:
                emit_signal("inventory_changed")
                return 0
    
    emit_signal("inventory_changed")
    return remaining_amount # Devuelve items que no se pudieron añadir

func remove_item(item_id: String, amount: int = 1) -> bool:
    var total_removed = 0
    var slots_to_clear: Array[InventorySlot] = []

    # Iterar desde el final para evitar problemas al eliminar
    for i in range(slots.size() - 1, -1, -1):
        var slot = slots[i]
        if not slot.is_empty() and slot.get_item_id() == item_id:
            var can_remove = min(amount - total_removed, slot.current_amount)
            if can_remove > 0:
                slot.remove_item(can_remove)
                total_removed += can_remove
                if slot.is_empty():
                    slots_to_clear.append(slot)

            if total_removed == amount:
                break
    
    if total_removed == amount:
        emit_signal("inventory_changed")
        return true
    
    # Si no se pudo remover la cantidad total, revertir o manejar el error
    # Por simplicidad, aquí asumimos que si no se pudo remover TODO, es un fallo.
    # En un juego real, quizás querrías remover lo que puedas.
    return false # No se pudo remover la cantidad completa

func get_item_count(item_id: String) -> int:
    var count = 0
    for slot in slots:
        if not slot.is_empty() and slot.get_item_id() == item_id:
            count += slot.current_amount
    return count

func has_item(item_id: String, amount: int = 1) -> bool:
    return get_item_count(item_id) >= amount

func get_slot(index: int) -> InventorySlot:
    if index >= 0 and index < slots.size():
        return slots[index]
    return null
🔥 Importante: La señal `inventory_changed` será crucial para actualizar nuestra interfaz de usuario cada vez que el contenido del inventario cambie.

📖 Paso 4: Creando la Interfaz de Usuario (UI) 🎨

Ahora que tenemos la lógica del inventario, necesitamos una forma de visualizarlo. Crearemos una escena para cada slot individual y luego una escena para la mochila completa que contendrá múltiples slots.

Escena InventorySlotUI.tscn

Crea una nueva escena de tipo Control y llámala InventorySlotUI.tscn. Guarda el script asociado en res/ui/InventorySlotUI.gd.

Estructura del Nodo:

- InventorySlotUI (Control)
  - Background (TextureRect) - para la apariencia del slot
  - ItemTexture (TextureRect) - para mostrar el icono del item
  - ItemAmount (Label) - para mostrar la cantidad del item

Estilo (TextureRect Background):

  • En el Inspector, para Background, busca Texture y puedes usar una textura cuadrada con un borde. Stretch Mode a Scale On Expand.
  • Ajusta el Min Size del InventorySlotUI (ej. (64, 64)) y el Background al mismo tamaño. Asegúrate de que Layout del Background esté en Full Rect.

Script InventorySlotUI.gd:

# res/ui/InventorySlotUI.gd
extends Control

@onready var item_texture: TextureRect = $ItemTexture
@onready var item_amount: Label = $ItemAmount

var assigned_slot: InventorySlot = null

func _ready():
    update_ui()

func assign_slot(slot: InventorySlot):
    assigned_slot = slot
    update_ui()

func update_ui():
    if assigned_slot and not assigned_slot.is_empty():
        item_texture.texture = assigned_slot.item_resource.texture
        item_texture.show()
        if assigned_slot.item_resource.is_stackable:
            item_amount.text = str(assigned_slot.current_amount)
            item_amount.show()
        else:
            item_amount.hide()
    else:
        item_texture.texture = null
        item_texture.hide()
        item_amount.hide()

Escena InventoryUI.tscn

Ahora, crea la escena principal del inventario. Nueva escena de tipo Control, llámala InventoryUI.tscn y guarda el script asociado en res/ui/InventoryUI.gd.

Estructura del Nodo:

- InventoryUI (Control)
  - Panel (PanelContainer) - Un contenedor para el inventario
    - MarginContainer (MarginContainer)
      - VBoxContainer (VBoxContainer)
        - Label (Label) - Título: "Mochila"
        - GridContainer (GridContainer) - Contendrá los InventorySlotUI

Configuración:

  • Ajusta el Panel para que tenga un tamaño y una posición razonables en la pantalla (ej. Rect -> Position = (200, 100), Rect -> Size = (300, 400)).
  • El GridContainer será donde se instancien dinámicamente nuestros InventorySlotUIs. Establece Columns a un número razonable, por ejemplo, 5.
  • Asegúrate de que el MarginContainer aplique un poco de margen (ej. 10px en todas las direcciones) y el VBoxContainer maneje el espaciado vertical.

Script InventoryUI.gd:

# res/ui/InventoryUI.gd
extends Control

@onready var grid_container: GridContainer = $Panel/MarginContainer/VBoxContainer/GridContainer
@export var inventory_slot_ui_prefab: PackedScene # Asignar InventorySlotUI.tscn aquí

var player_inventory: Inventory = null

func _ready():
    hide() # Ocultar al inicio

func set_inventory(inventory_resource: Inventory):
    if player_inventory:
        player_inventory.inventory_changed.disconnect(self._on_inventory_changed)
    
    player_inventory = inventory_resource
    if player_inventory:
        player_inventory.inventory_changed.connect(self._on_inventory_changed)
        _on_inventory_changed() # Actualizar UI inmediatamente

func _on_inventory_changed():
    _draw_inventory()

func _draw_inventory():
    # Limpiar slots antiguos
    for child in grid_container.get_children():
        child.queue_free()
    
    if player_inventory:
        # Crear y añadir nuevos slots
        for slot_data in player_inventory.slots:
            var slot_ui = inventory_slot_ui_prefab.instantiate() as InventorySlotUI
            grid_container.add_child(slot_ui)
            slot_ui.assign_slot(slot_data)

func _input(event: InputEvent):
    if event.is_action_pressed("toggle_inventory"): # Necesitas configurar esta acción en Project Settings -> Input Map
        if is_visible():
            hide()
        else:
            show()
            _on_inventory_changed() # Asegurarse de que esté actualizado al mostrar
💡 Consejo: No olvides arrastrar el `InventorySlotUI.tscn` al campo `inventory_slot_ui_prefab` en el inspector del nodo `InventoryUI` una vez lo tengas creado.

📖 Paso 5: Integración con el Juego (Main Scene) 🎮

Finalmente, necesitamos integrar nuestro inventario en la escena principal del juego.

Escena Main.tscn (o tu escena de juego principal)

Crea una escena principal o usa una existente. Añade un nodo Node o CharacterBody2D/3D para tu jugador. Añade un nodo CanvasLayer para la UI. Instancia tu InventoryUI.tscn como hijo del CanvasLayer.

Estructura:

- Main (Node2D)
  - Player (CharacterBody2D)
    - Player.gd
  - CanvasLayer
    - InventoryUI (Control)

Script Player.gd (o similar):

Modifica el script de tu jugador para que tenga una instancia del inventario y pueda interactuar con él. También, gestionaremos la asignación del inventario a la UI.

# Player.gd
extends CharacterBody2D # O tu nodo de jugador

@export var initial_inventory_size: int = 10
@export var inventory_resource_path: String = "res://inventory/player_inventory.tres" # Ruta para guardar/cargar

var player_inventory: Inventory
var inventory_ui: InventoryUI

func _ready():
    _setup_inventory()
    
    # Enlazar la UI del inventario
    inventory_ui = get_tree().get_first_node_in_group("inventory_ui") as InventoryUI
    if inventory_ui:
        inventory_ui.set_inventory(player_inventory)
    else:
        print("Advertencia: No se encontró InventoryUI con el grupo 'inventory_ui'.")
    
    # Ejemplo de cómo añadir items al inicio
    var potion_item = load("res://items/Potion.tres") as ItemResource
    var sword_item = load("res://items/Sword.tres") as ItemResource
    
    player_inventory.add_item(potion_item, 3)
    player_inventory.add_item(sword_item, 1)
    player_inventory.add_item(potion_item, 2)
    player_inventory.add_item(sword_item, 1) # Esto no debería añadirlo si la espada no es stackable

func _setup_inventory():
    if ResourceLoader.exists(inventory_resource_path):
        player_inventory = load(inventory_resource_path) as Inventory
        print("Inventario cargado.")
    else:
        player_inventory = Inventory.new(initial_inventory_size)
        player_inventory.resource_path = inventory_resource_path # Guardar en este path
        player_inventory.resource_saver_save(player_inventory, inventory_resource_path) # Guardar por primera vez
        print("Nuevo inventario creado y guardado.")

func _input(event: InputEvent):
    if event.is_action_pressed("test_add_item"): # Otra acción, por ejemplo "E"
        var potion_item = load("res://items/Potion.tres") as ItemResource
        var remaining = player_inventory.add_item(potion_item, 1)
        if remaining == 0:
            print("Poción añadida.")
        else:
            print("Inventario lleno, no se pudo añadir: ", remaining, " pociones.")
    
    if event.is_action_pressed("test_remove_item"): # Por ejemplo "R"
        if player_inventory.remove_item("potion_health", 1):
            print("Poción removida.")
        else:
            print("No hay pociones para remover.")
⚠️ Advertencia: Asegúrate de que tu `InventoryUI` esté en un grupo llamado `inventory_ui` (`Node -> Groups` en el inspector) para que el `Player.gd` pueda encontrarlo fácilmente. También, configura las acciones `toggle_inventory`, `test_add_item` y `test_remove_item` en `Project -> Project Settings -> Input Map`.

📈 Diagrama de Flujo del Sistema de Inventario

Aquí tienes una representación visual de cómo interactúan los diferentes componentes de nuestro sistema de inventario:

ItemResource InventorySlot Inventory (Recurso) InventorySlotUI InventoryUI Player / Game Contiene Gestiona Visualiza Renderiza Interactúa con Usa/Modifica Contiene (instancias)

Estructura Interfaz Lógica Principal Interacción


🏁 Conclusión y Próximos Pasos

¡Felicidades! 🎉 Has construido un sistema de inventario básico pero funcional en Godot 4. Ahora tus jugadores pueden recolectar y organizar objetos en su mochila. Hemos cubierto la definición de objetos, la gestión de slots, la lógica del inventario y la interfaz de usuario.

Este sistema es una base sólida sobre la cual puedes construir. Aquí tienes algunas ideas para expandirlo:

  • Arrastrar y Soltar (Drag & Drop): Implementa la funcionalidad para mover items entre slots.
  • Uso de Items: Añade lógica para que al hacer clic derecho o doble clic, un item se "use" (ej. una poción restaura vida).
  • Equipamiento: Crea slots específicos para equipar armas, armaduras, etc.
  • Persistencia: Mejora el guardado y carga del inventario usando JSON o el sistema de guardado nativo de Godot.
  • Filtrado/Clasificación: Permite a los jugadores ordenar el inventario por tipo, nombre, etc.
  • Inventario del Mundo: Añade lógica para que los objetos puedan ser recogidos del suelo.
🔥 Importante: La modularidad de los recursos (`ItemResource`, `InventorySlot`, `Inventory`) es clave para la escalabilidad y facilidad de mantenimiento de tu proyecto.

¡Sigue experimentando y construyendo! El desarrollo de videojuegos es un viaje de aprendizaje continuo. Si tienes preguntas o mejoras, no dudes en compartirlas. ¡Feliz desarrollo con Godot! 🚀

Comentarios (0)

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