Unreal Engine: Creando un Sistema de Inventario Dinámico con UMG y C++
Este tutorial te guiará paso a paso en la creación de un robusto sistema de inventario para tus juegos en Unreal Engine 5. Aprenderás a combinar la potencia de C++ para la lógica de datos y la flexibilidad de UMG para diseñar una interfaz de usuario interactiva y atractiva. Cubriremos la gestión de objetos, la visualización y la interacción del jugador.
🚀 Introducción al Sistema de Inventario en Unreal Engine
El sistema de inventario es un componente fundamental en la mayoría de los videojuegos modernos, especialmente en géneros como RPG, supervivencia o aventura. Permite a los jugadores recolectar, almacenar y gestionar objetos, lo que añade profundidad y estrategia a la jugabilidad. En este tutorial, exploraremos cómo construir un sistema de inventario dinámico y eficiente utilizando las herramientas de Unreal Engine 5: la potencia de C++ para el backend y la versatilidad de Unreal Motion Graphics (UMG) para el frontend.
¿Por qué C++ y UMG?
Combinar C++ con UMG nos ofrece lo mejor de ambos mundos:
- C++: Proporciona un rendimiento óptimo, mayor control sobre la memoria y acceso a todas las características del motor. Es ideal para la lógica de datos subyacente del inventario, la gestión de objetos y la comunicación con otros sistemas del juego.
- UMG: Es el sistema de interfaz de usuario de Unreal Engine, que permite crear interfaces visualmente atractivas e interactivas de forma rápida y eficiente. Perfecto para diseñar la representación visual del inventario, los slots de objetos y las herramientas de interacción del usuario.
🛠️ Configuración Inicial del Proyecto
Antes de sumergirnos en la lógica, necesitamos configurar nuestro proyecto.
1. Creación del Proyecto
Comienza creando un nuevo proyecto de Unreal Engine 5. Puedes elegir una plantilla de juego en blanco (Blank) o la plantilla de tercera persona (Third Person) si quieres un personaje preconfigurado para probar el inventario.
2. Estructura de Carpetas Recomendada
Una buena organización es clave para proyectos grandes. Sugerimos la siguiente estructura en la carpeta Content:
Content/InventoryData(para estructuras de datos, data assets)Items(para blueprints de objetos específicos)Widgets(para todos los widgets UMG)C++(donde irán tus clases C++ del inventario)
¿Por qué es importante la organización de carpetas?
Una estructura de carpetas clara facilita la navegación, el mantenimiento del proyecto y el trabajo en equipo. Previene la "espiral de la desorganización" que puede surgir en proyectos grandes.📖 Paso 1: Definiendo los Datos del Objeto (C++)
El primer paso es definir qué es un objeto en nuestro inventario. Usaremos una estructura de datos para almacenar la información básica de cada ítem.
1.1. Creando la Clase Base de Objeto (UObject)
Crearemos una clase base UInventoryItem que herede de UObject. Esto nos permitirá tener instancias de objetos de inventario gestionadas por el recolector de basura de Unreal y replicarlas fácilmente si el juego es multijugador.
En el Editor de Unreal, ve a Tools > New C++ Class.... Selecciona UObject como clase padre y nómbrala InventoryItem (o InventoryObject para evitar conflictos con AActor).
Archivo InventoryItem.h:
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "InventoryItem.generated.h"
UCLASS(Blueprintable, BlueprintType)
class YOURPROJECT_API UInventoryItem : public UObject
{
GENERATED_BODY()
public:
UInventoryItem();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item Info")
FText ItemName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item Info")
FText ItemDescription;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item Info")
UTexture2D* Thumbnail;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item Info")
int32 Weight;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item Info")
int32 MaxStackSize; // Cuántos de este item se pueden apilar
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item Info")
bool bIsStackable; // ¿Se puede apilar este item?
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Runtime Info")
int32 CurrentStackSize; // Cantidad actual de este item en un slot
void SetCurrentStackSize(int32 NewSize) { CurrentStackSize = NewSize; }
};
Archivo InventoryItem.cpp:
#include "InventoryItem.h"
UInventoryItem::UInventoryItem()
: ItemName(FText::FromString("New Item")),
ItemDescription(FText::FromString("Generic item description")),
Thumbnail(nullptr),
Weight(1),
MaxStackSize(1),
bIsStackable(false),
CurrentStackSize(1)
{
}
📋 Paso 2: Creando el Componente de Inventario (C++)
Ahora necesitamos un lugar donde almacenar estos objetos. Crearemos un UActorComponent que se pueda añadir a cualquier actor (como el jugador) para darle capacidades de inventario.
2.1. Creando el Componente
Crea una nueva clase C++ que herede de UActorComponent y nómbrala InventoryComponent.
Archivo InventoryComponent.h:
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InventoryItem.h"
#include "InventoryComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryUpdated, const TArray<UInventoryItem*>&, CurrentInventory);
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class YOURPROJECT_API UInventoryComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInventoryComponent();
protected:
virtual void BeginPlay() override;
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory")
int32 Capacity; // Número máximo de slots
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventory")
TArray<UInventoryItem*> Items; // Los items actuales en el inventario
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool AddItem(UInventoryItem* ItemToAdd);
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool RemoveItem(UInventoryItem* ItemToRemove);
UFUNCTION(BlueprintCallable, Category = "Inventory")
void ClearInventory();
// Delegate para notificar a la UI cuando el inventario cambia
UPROPERTY(BlueprintAssignable, Category = "Inventory")
FOnInventoryUpdated OnInventoryUpdated;
void NotifyInventoryUpdated();
protected:
bool TryStackItem(UInventoryItem* ItemToAdd);
void AddNewItem(UInventoryItem* ItemToAdd);
};
Archivo InventoryComponent.cpp:
#include "InventoryComponent.h"
#include "Net/UnrealNetwork.h"
UInventoryComponent::UInventoryComponent()
{
PrimaryComponentTick.bCanEverTick = false;
Capacity = 10; // Capacidad inicial
SetIsReplicatedByDefault(true); // Es importante para multijugador
}
void UInventoryComponent::BeginPlay()
{
Super::BeginPlay();
// Inicializar el array de ítems con punteros nulos para representar slots vacíos
Items.Init(nullptr, Capacity);
}
void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UInventoryComponent, Items);
DOREPLIFETIME(UInventoryComponent, Capacity);
}
void UInventoryComponent::NotifyInventoryUpdated()
{
OnInventoryUpdated.Broadcast(Items);
}
bool UInventoryComponent::AddItem(UInventoryItem* ItemToAdd)
{
if (!ItemToAdd) return false;
// Si es apilable, intenta apilarlo
if (ItemToAdd->bIsStackable && TryStackItem(ItemToAdd))
{
NotifyInventoryUpdated();
return true;
}
// Si no es apilable o no se pudo apilar, busca un slot vacío
int32 EmptySlotIndex = Items.Find(nullptr);
if (EmptySlotIndex != INDEX_NONE && EmptySlotIndex < Capacity)
{
// Creamos una nueva instancia del ítem para que cada slot tenga su propia instancia
// Esto es crucial para manejar CurrentStackSize de forma individual
UInventoryItem* NewInstance = DuplicateObject<UInventoryItem>(ItemToAdd, this);
NewInstance->SetCurrentStackSize(ItemToAdd->bIsStackable ? ItemToAdd->CurrentStackSize : 1);
Items[EmptySlotIndex] = NewInstance;
NotifyInventoryUpdated();
return true;
}
// No hay slots disponibles
return false;
}
bool UInventoryComponent::RemoveItem(UInventoryItem* ItemToRemove)
{
if (!ItemToRemove) return false;
int32 ItemIndex = Items.Find(ItemToRemove);
if (ItemIndex != INDEX_NONE)
{
// Si el ítem es apilable y tiene más de uno, solo decrementamos la cantidad
if (ItemToRemove->bIsStackable && ItemToRemove->CurrentStackSize > 1)
{
ItemToRemove->SetCurrentStackSize(ItemToRemove->CurrentStackSize - 1);
}
else // Si es uno solo o no apilable, lo eliminamos del slot
{
Items[ItemIndex] = nullptr;
ItemToRemove->MarkAsGarbage(); // Marcar para eliminación por el GC
}
NotifyInventoryUpdated();
return true;
}
return false;
}
void UInventoryComponent::ClearInventory()
{
for (UInventoryItem* Item : Items)
{
if (Item) Item->MarkAsGarbage();
}
Items.Init(nullptr, Capacity);
NotifyInventoryUpdated();
}
bool UInventoryComponent::TryStackItem(UInventoryItem* ItemToAdd)
{
for (UInventoryItem* ExistingItem : Items)
{
if (ExistingItem && ExistingItem->GetClass() == ItemToAdd->GetClass() && ExistingItem->bIsStackable)
{
int32 SpaceLeft = ExistingItem->MaxStackSize - ExistingItem->CurrentStackSize;
if (SpaceLeft > 0)
{
int32 AmountToStack = FMath::Min(SpaceLeft, ItemToAdd->CurrentStackSize);
ExistingItem->SetCurrentStackSize(ExistingItem->CurrentStackSize + AmountToStack);
ItemToAdd->SetCurrentStackSize(ItemToAdd->CurrentStackSize - AmountToStack);
if (ItemToAdd->CurrentStackSize <= 0)
{
ItemToAdd->MarkAsGarbage(); // El ítem original fue completamente apilado
return true;
}
// Si aún quedan ítems en ItemToAdd, intentará apilarlos en otro slot o añadirlos como nuevo
}
}
}
return false; // No se pudo apilar completamente o no se encontró un stack compatible
}
🎨 Paso 3: Diseño de la Interfaz de Usuario (UMG)
Ahora crearemos los widgets UMG para visualizar y permitir la interacción con nuestro inventario.
3.1. Widget de Slot de Inventario (WBP_InventorySlot)
Este widget representará un único slot en el inventario, mostrando el icono del ítem y su cantidad.
-
En la carpeta
Content/Inventory/Widgets, crea un nuevo User Widget y nómbraloWBP_InventorySlot. -
Abre
WBP_InventorySlot. -
Jerarquía de Widgets:
CanvasPanel(Root)SizeBox(para tamaño fijo del slot, e.g., 64x64)Border(fondo visual del slot)OverlayImage(para el icono del ítem, nombre:ItemThumbnail)TextBlock(para la cantidad del ítem, nombre:ItemQuantity, alineado abajo a la derecha)
-
En el Graph de
WBP_InventorySlot:- Crea una variable
UInventoryItem* ItemReference(Editable, Expose on Spawn). - En
Event Pre Construct, actualiza la visibilidad del icono y la cantidad basándose enItemReference.
- Crea una variable
3.2. Widget de Inventario Principal (WBP_InventoryScreen)
Este será el widget principal que contendrá todos los slots del inventario.
-
En la carpeta
Content/Inventory/Widgets, crea un nuevo User Widget y nómbraloWBP_InventoryScreen. -
Abre
WBP_InventoryScreen. -
Jerarquía de Widgets:
CanvasPanel(Root)Border(fondo del inventario)VerticalBoxTextBlock(Título: "Inventario")UniformGridPanel(nombre:InventoryGrid, donde se añadirán losWBP_InventorySlotdinámicamente)
-
En el Graph de
WBP_InventoryScreen:- Crea una variable
UInventoryComponent* OwningInventory(Editable). - Crea una función
RefreshInventoryUI. - Dentro de
RefreshInventoryUI:- Limpia el
InventoryGrid. - Itera sobre
OwningInventory->Items. - Por cada ítem:
- Crea un
WBP_InventorySlot. - Pasa el ítem actual al
WBP_InventorySlot(Set ItemReference). - Añade el
WBP_InventorySlotalInventoryGrid.
- Crea un
- Limpia el
- Crea una variable
- En el
Event ConstructdeWBP_InventoryScreen:- Asegúrate de que
OwningInventoryesté asignado (por ejemplo, desde el personaje). - Llama a
RefreshInventoryUI. - Suscríbete al delegado
OnInventoryUpdateddeOwningInventoryy conéctalo aRefreshInventoryUI.
- Asegúrate de que
🔗 Paso 4: Conectando C++ con UMG
Ahora enlazaremos la lógica de C++ con la interfaz UMG.
4.1. Creando un Item de Blueprint Derivado
- Ve a tu carpeta
Content/Inventory/Items. - Haz clic derecho ->
Blueprint Class. - En
All Classes, busca y seleccionaInventoryItem(tu clase C++). - Nómbralo
BP_HealthPotion. - Abre
BP_HealthPotiony configura sus propiedades por defecto:Item Name: "Poción de Salud"Item Description: "Restaura 50 puntos de vida."Thumbnail: Asigna una textura (puedes usar una de los starter content o crear una simple).Weight: 1Max Stack Size: 5 (si quieres que sea apilable)Is Stackable: true
4.2. Añadiendo el Inventario al Personaje (Blueprint)
- Abre tu Blueprint de personaje (e.g.,
BP_ThirdPersonCharacter). - En la pestaña
Components, haz clic enAddy buscaInventoryComponent. - Asigna la capacidad deseada a tu
InventoryComponent.
4.3. Mostrando el Inventario en Pantalla
En tu Blueprint de personaje:
- En el
Event Graph, busca un evento de entrada (e.g.,Input Action (E)oInput Action (I)para Inventario). - Cuando se presione la tecla:
- Comprueba si el widget de inventario ya está creado. Si no, créalo.
- Si no es visible, añádelo al viewport y muestra el cursor del ratón.
- Si ya es visible, quítalo del viewport y oculta el cursor.
- Importante: Cuando crees
WBP_InventoryScreen, asegúrate de pasarle la referencia a tuInventoryComponent(la variableOwningInventory).
✨ Paso 5: Implementando la Recolección de Ítems
Para que el inventario sea útil, necesitamos poder añadir ítems a él.
5.1. Clase Base de Recolectable (Actor)
Crea una nueva clase C++ que herede de AActor y nómbrala CollectibleActor.
Archivo CollectibleActor.h:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "InventoryItem.h"
#include "CollectibleActor.generated.h"
UCLASS()
class YOURPROJECT_API ACollectibleActor : public AActor
{
GENERATED_BODY()
public:
ACollectibleActor();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UStaticMeshComponent* MeshComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
class USphereComponent* SphereComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
TSubclassOf<UInventoryItem> ItemClass;
protected:
virtual void BeginPlay() override;
UFUNCTION()
void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
public:
// Función para obtener el ítem instanciado por este recolectable
UFUNCTION(BlueprintCallable, Category = "Item")
UInventoryItem* GetItemInstance();
protected:
// Instancia del ítem que será añadida al inventario
UPROPERTY()
UInventoryItem* ItemInstance;
};
Archivo CollectibleActor.cpp:
#include "CollectibleActor.h"
#include "Components/StaticMeshComponent.h"
#include "Components/SphereComponent.h"
#include "InventoryComponent.h"
#include "Kismet/GameplayStatics.h"
ACollectibleActor::ACollectibleActor()
{
PrimaryActorTick.bCanEverTick = false;
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
RootComponent = MeshComponent;
SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
SphereComponent->SetupAttachment(RootComponent);
SphereComponent->SetSphereRadius(50.f);
SphereComponent->OnComponentBeginOverlap.AddDynamic(this, &ACollectibleActor::OnOverlapBegin);
ItemClass = UInventoryItem::StaticClass(); // Clase por defecto
}
void ACollectibleActor::BeginPlay()
{
Super::BeginPlay();
if (ItemClass)
{
ItemInstance = NewObject<UInventoryItem>(this, ItemClass);
}
}
void ACollectibleActor::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor && OtherActor != this && ItemInstance)
{
UInventoryComponent* Inventory = OtherActor->FindComponentByClass<UInventoryComponent>();
if (Inventory)
{
if (Inventory->AddItem(ItemInstance))
{
// Si el ítem se añadió con éxito, destruir este actor recolectable
Destroy();
}
}
}
}
UInventoryItem* ACollectibleActor::GetItemInstance()
{
return ItemInstance;
}
5.2. Creando un Blueprint de Recolectable
- Crea un nuevo Blueprint Class basado en
CollectibleActorenContent/Inventory/Items. - Nómbralo
BP_Collectible_HealthPotion. - Abre
BP_Collectible_HealthPotion.- Asigna un
Static MeshaMeshComponent(e.g., una esfera o un cubo del starter content). - Asegúrate de que
Item Classesté configurado aBP_HealthPotion(la clase de ítem de blueprint que creaste antes).
- Asigna un
- Arrastra algunas instancias de
BP_Collectible_HealthPotional nivel.
🎮 Paso 6: Interacción y Funcionalidad Adicional
Un inventario no solo muestra ítems, también permite interactuar con ellos.
6.1. Funcionalidad de 'Usar Ítem' (UInventoryItem)
Podemos añadir una función virtual UseItem a nuestra clase base UInventoryItem para que los ítems puedan tener efectos.
Archivo InventoryItem.h (actualizado):
// ... (código anterior)
UCLASS(Blueprintable, BlueprintType)
class YOURPROJECT_API UInventoryItem : public UObject
{
GENERATED_BODY()
public:
UInventoryItem();
// ... (propiedades anteriores)
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Item Actions")
void UseItem(AActor* Character);
virtual void UseItem_Implementation(AActor* Character);
};
Archivo InventoryItem.cpp (actualizado):
// ... (código anterior)
void UInventoryItem::UseItem_Implementation(AActor* Character)
{
// Lógica por defecto para usar un ítem
UE_LOG(LogTemp, Warning, TEXT("Using item: %s"), *ItemName.ToString());
}
Ahora, en tu BP_HealthPotion (el Blueprint de ítem), puedes sobrescribir el evento UseItem_Implementation para añadir la lógica específica, como curar al personaje. Luego, en InventoryComponent, puedes crear una función UseItemInSlot que llame a este método y luego a RemoveItem.
6.2. Funcionalidad de Arrastrar y Soltar (Drag & Drop)
Implementar Drag & Drop es más complejo y requiere:
- UMG Drag & Drop Operation: Una clase UMG que define qué sucede cuando se arrastra un ítem.
- Widgets que pueden arrastrar: Tu
WBP_InventorySlotnecesita un eventoOnMouseButtonDownpara iniciar la operación de arrastre. - Widgets que pueden soltar: Tu
WBP_InventorySlotyWBP_InventoryScreennecesitaránOnDroppara manejar dónde se suelta el ítem (moverse entre slots, soltar fuera para descartar).
Este es un tema avanzado por sí mismo, pero es fundamental para un inventario moderno.
Ejemplo básico de Drag & Drop para un slot
En WBP_InventorySlot Graph:
EVENT OnMouseButtonDown (Left Mouse Button)
IF ItemReference IS VALID
Create Drag and Drop Operation (MyDragDropOperation)
SET Payload (MyDragDropOperation) TO ItemReference
SET DefaultDragVisual (MyDragDropOperation) TO CreateWidget(WBP_ItemDragVisual)
DO Drag (MyDragDropOperation)
END IF
END EVENT
En WBP_InventorySlot Graph (para soltar un ítem en otro slot):
EVENT OnDrop
CAST DragAndDropOperation TO MyDragDropOperation
IF CAST IS SUCCESSFUL
GET ItemReference FROM MyDragDropOperation (This is the dragged item)
// Lógica para intentar mover el itemDragged al slot de este widget
// Esto probablemente requerirá llamar a funciones en el OwningInventoryComponent
END IF
END EVENT
6.3. Mejoras Avanzadas
- Replicación de Inventario: Asegúrate de que todas las modificaciones del inventario (añadir/quitar ítems, cambiar cantidades) se realicen en el servidor para juegos multijugador y se repliquen a los clientes.
- Persistencia: Guarda y carga el estado del inventario utilizando el sistema de guardado de Unreal Engine (
USaveGame). - Información Detallada (Tooltips): Crea un widget adicional que muestre información detallada del ítem cuando el jugador pase el ratón por encima de un slot.
- Equipamiento: Un sistema para equipar ítems en ranuras específicas (armas, armaduras, accesorios).
✅ Conclusión
Has llegado al final de este extenso tutorial sobre cómo crear un sistema de inventario dinámico en Unreal Engine 5, combinando la potencia de C++ para la lógica de datos y la flexibilidad de UMG para la interfaz de usuario. Hemos cubierto la definición de ítems, la creación del componente de inventario, el diseño de los widgets y la implementación básica de la recolección.
Este es solo el comienzo. Un sistema de inventario es un componente complejo que puede expandirse con muchas más características, como sistemas de equipamiento, crafteo, almacenamiento, comercio y más. ¡Experimenta, itera y adapta estas bases a las necesidades específicas de tu juego!
Tutoriales relacionados
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!