tutoriales.com

Creating a Basic Inventory System in Godot 4: Item and Backpack Management

This tutorial will guide you through the essential steps to build a functional inventory system in Godot 4. You'll learn how to define items, manage inventory slots, and create an interactive user interface for your player's backpack.

Intermedio20 min de lectura216 views
Reportar error

Hey there, game developer! Are you ready to empower your players to collect, use, and manage items in your Godot 4 game? An inventory system is fundamental to almost any RPG, adventure game, or survival title. In this tutorial, we'll build a basic yet robust inventory system from scratch.

Here, we'll focus on the logic behind items, inventory slots, and how the user interface interacts with these components. Get ready to organize your pixels!

💡 Tip: Familiarity with basic GDScript and the Godot 4 editor will greatly help you follow this tutorial.

🎯 What will we learn in this tutorial?

  • Define the structure of our items using a custom resource (Resource).
  • Create an Inventory system that manages slots.
  • Implement a user interface for the player's backpack (UI).
  • Add and remove items from the inventory.
  • Visualize items in the user interface.

📖 Step 1: Defining an Item (ItemResource) ✨

The first step is to define what an item is in our game. In Godot, the best way to do this is by using a Resource. This allows us to create item templates that can be instantiated and used anywhere in the game without needing a node in the scene.

Creating ItemResource.gd

Create a new script named ItemResource.gd in a res/items folder (or wherever you prefer) and paste the following code:

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

@export_category("Item Data")
@export var id: String = "new_item" # Unique item identifier
@export var item_name: String = "Nuevo Objeto" # Visible name
@export var description: String = "Una descripción genial para este objeto." # Item description
@export var texture: Texture2D # Icon texture
@export var max_stack_size: int = 1 # How many items can stack in one slot
@export var is_stackable: bool = false # Whether the item can be stacked or not

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
📌 Note: We use `@export` so these properties are editable directly in the Godot inspector when we create a new `ItemResource`. `class_name` allows Godot to recognize this resource type by its name.

Creating Item Resources (Examples)

Now, you can create some example items. Right-click on the res/items folder (or the one you're using) in the Filesystem, select New Resource..., and search for ItemResource. Create at least two:

  1. res/items/Potion.tres:

    • id: potion_health
    • item_name: Health Potion
    • description: Restores a bit of health.
    • texture: Assign a texture (you can create a simple one with a red circle).
    • max_stack_size: 5
    • is_stackable: ✅
  2. res/items/Sword.tres:

    • id: rusty_sword
    • item_name: Rusty Sword
    • description: An old sword, not very sharp.
    • texture: Assign a texture (a sword icon).
    • max_stack_size: 1
    • is_stackable: ❌

📖 Step 2: The Inventory Slot (InventorySlot.gd) 📦

Each space in our inventory will be an InventorySlot. This resource will hold a reference to the ItemResource and the quantity of that item in the slot.

Creating InventorySlot.gd

Create an InventorySlot.gd script in 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 # All items were added

    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 # Returns the remaining amount that couldn't be added
    return amount # Nothing could be added if not stackable or doesn't match

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 # Slot was emptied and item type is returned

    return removed_item # Items were removed, but the slot was not emptied

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 "Empty"

📖 Step 3: The Inventory System (Inventory.gd) 👜

Now we'll create the heart of our system: the Inventory itself. This script will manage a collection of InventorySlots and provide the logic for adding, removing, and searching for items.

Creating Inventory.gd

Create Inventory.gd in res/inventory:

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

@export var inventory_size: int = 10 # Number of slots in the inventory
@export var slots: Array[InventorySlot] # Array of slots

signal inventory_changed # Emitted when the inventory changes

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

    # Try to add to existing slots (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
    
    # If items remain, try to add to empty slots
    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 # Returns items that couldn't be added

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

    # Iterate from the end to avoid issues when deleting
    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
    
    # If the total amount couldn't be removed, revert or handle the error
    # For simplicity, here we assume that if ALL couldn't be removed, it's a failure.
    # In a real game, you might want to remove what you can.
    return false # The full amount could not be removed

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
🔥 Important: The `inventory_changed` signal will be crucial for updating our user interface every time the inventory's contents change.

📖 Step 4: Creating the User Interface (UI) 🎨

Now that we have the inventory logic, we need a way to visualize it. We'll create a scene for each individual slot and then a scene for the entire backpack that will contain multiple slots.

InventorySlotUI.tscn Scene

Create a new scene of type Control and name it InventorySlotUI.tscn. Save the associated script in res/ui/InventorySlotUI.gd.

Node Structure:

- InventorySlotUI (Control)
  - Background (TextureRect) - for the slot's appearance
  - ItemTexture (TextureRect) - to display the item icon
  - ItemAmount (Label) - to display the item quantity

Styling (TextureRect Background):

  • In the Inspector, for Background, look for Texture and you can use a square texture with a border. Set Stretch Mode to Scale On Expand.
  • Adjust the Min Size of InventorySlotUI (e.g., (64, 64)) and Background to the same size. Make sure the Layout of Background is set to Full Rect.

InventorySlotUI.gd Script:

# 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()

InventoryUI.tscn Scene

Now, create the main inventory scene. New scene of type Control, name it InventoryUI.tscn, and save the associated script in res/ui/InventoryUI.gd.

Node Structure:

- InventoryUI (Control)
  - Panel (PanelContainer) - A container for the inventory
    - MarginContainer (MarginContainer)
      - VBoxContainer (VBoxContainer)
        - Label (Label) - Title: "Backpack"
        - GridContainer (GridContainer) - Will contain the InventorySlotUI

Configuration:

  • Adjust the Panel to have a reasonable size and position on the screen (e.g., Rect -> Position = (200, 100), Rect -> Size = (300, 400)).
  • The GridContainer will be where our InventorySlotUIs are dynamically instantiated. Set Columns to a reasonable number, for example, 5.
  • Ensure the MarginContainer applies some margin (e.g., 10px in all directions) and the VBoxContainer handles vertical spacing.

InventoryUI.gd Script:

# res/ui/InventoryUI.gd
extends Control

@onready var grid_container: GridContainer = $Panel/MarginContainer/VBoxContainer/GridContainer
@export var inventory_slot_ui_prefab: PackedScene # Assign InventorySlotUI.tscn here

var player_inventory: Inventory = null

func _ready():
    hide() # Hide at startup

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() # Update UI immediately

func _on_inventory_changed():
    _draw_inventory()

func _draw_inventory():
    # Clear old slots
    for child in grid_container.get_children():
        child.queue_free()
    
    if player_inventory:
        # Create and add new 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"): # You need to configure this action in Project Settings -> Input Map
        if is_visible():
            hide()
        else:
            show()
            _on_inventory_changed() # Ensure it's updated when shown
💡 Tip: Don't forget to drag `InventorySlotUI.tscn` to the `inventory_slot_ui_prefab` field in the `InventoryUI` node's inspector once you've created it.

📖 Step 5: Integrating with the Game (Main Scene) 🎮

Finally, we need to integrate our inventory into the main game scene.

Main.tscn Scene (or your main game scene)

Create a main scene or use an existing one. Add a Node or CharacterBody2D/3D node for your player. Add a CanvasLayer node for the UI. Instance your InventoryUI.tscn as a child of the CanvasLayer.

Structure:

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

Player.gd Script (or similar):

Modify your player script to have an inventory instance and be able to interact with it. We'll also handle assigning the inventory to the UI.

# Player.gd
extends CharacterBody2D # Or your player node

@export var initial_inventory_size: int = 10
@export var inventory_resource_path: String = "res://inventory/player_inventory.tres" # Path to save/load

var player_inventory: Inventory
var inventory_ui: InventoryUI

func _ready():
    _setup_inventory()
    
    # Link the inventory UI
    inventory_ui = get_tree().get_first_node_in_group("inventory_ui") as InventoryUI
    if inventory_ui:
        inventory_ui.set_inventory(player_inventory)
    else:
        print("Warning: InventoryUI with group 'inventory_ui' not found.")
    
    # Example of how to add items at the start
    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) # This should not add it if the sword is not stackable

func _setup_inventory():
    if ResourceLoader.exists(inventory_resource_path):
        player_inventory = load(inventory_resource_path) as Inventory
        print("Inventory loaded.")
    else:
        player_inventory = Inventory.new(initial_inventory_size)
        player_inventory.resource_path = inventory_resource_path # Save to this path
        player_inventory.resource_saver_save(player_inventory, inventory_resource_path) # Save for the first time
        print("New inventory created and saved.")

func _input(event: InputEvent):
    if event.is_action_pressed("test_add_item"): # Another action, for example "E"
        var potion_item = load("res://items/Potion.tres") as ItemResource
        var remaining = player_inventory.add_item(potion_item, 1)
        if remaining == 0:
            print("Potion added.")
        else:
            print("Inventory full, could not add: ", remaining, " potions.")
    
    if event.is_action_pressed("test_remove_item"): # For example "R"
        if player_inventory.remove_item("potion_health", 1):
            print("Potion removed.")
        else:
            print("No potions to remove.")
⚠️ Warning: Make sure your `InventoryUI` is in a group called `inventory_ui` (`Node -> Groups` in the inspector) so that `Player.gd` can find it easily. Also, configure the `toggle_inventory`, `test_add_item`, and `test_remove_item` actions in `Project -> Project Settings -> Input Map`.

📈 Inventory System Flow Diagram

Here's a visual representation of how the different components of our inventory system interact:

ItemResource InventorySlot Inventory (Resource) InventorySlotUI InventoryUI Player / Game Contains Manages Visualizes Renders Interacts with Uses/Modifies Contains (instances)

Inventory System Flow Diagram: This diagram illustrates the interaction between ItemResource, InventorySlot, Inventory (Resource), InventorySlotUI, InventoryUI, and Player / Game components.

Structure Interface Core Logic Interaction


🏁 Conclusion and Next Steps

Congratulations! 🎉 You've built a basic yet functional inventory system in Godot 4. Now your players can collect and organize items in their backpack. We've covered item definition, slot management, inventory logic, and the user interface.

This system is a solid foundation upon which you can build. Here are some ideas to expand it:

  • Drag & Drop: Implement functionality to move items between slots.
  • Item Usage: Add logic so that right-clicking or double-clicking an item

Tutoriales relacionados

Comentarios (0)

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