Como Criar Sistema de Inventário para Jogos RPG: Guia Completo

Aprenda a desenvolver sistemas de inventário profissionais para RPGs. Tutorial completo com código, UI/UX, otimização e melhores práticas para Godot e Unity.
Como Criar Sistema de Inventário para Jogos RPG: Guia Completo
Quase todo dev que começa um RPG subestima o inventário. Parece simples: uma lista de itens, um botão pra equipar, pronto. Aí chega a hora de empilhar poção com poção, arrastar uma espada de dois slots, salvar o estado, atualizar a UI sem travar o frame, e o "simples" vira o sistema mais chato de manter do projeto inteiro.
Esse guia mostra como montar um inventário que aguenta crescer. Vou separar dados, regra de negócio e interface, mostrar código que roda de verdade em Godot e Unity, e apontar onde os iniciantes costumam se enforcar. A ideia não é te dar a arquitetura "perfeita", é te dar uma base que você entende e consegue estender quando o jogo pedir.
O Que Um Inventário Precisa Resolver
Um inventário não é só onde os itens ficam guardados. Ele comunica progressão (o jogador vê o equipamento melhorando), força decisão (espaço limitado obriga escolha) e carrega economia (o que vale a pena carregar, o que vende). Quando você projeta pensando nessas três funções, as decisões técnicas ficam mais óbvias.
Olha como jogos diferentes resolvem isso, porque a escolha define o seu código:
- Diablo / Path of Exile: grid 2D onde cada item ocupa um retângulo de slots. O inventário vira um quebra-cabeça espacial. Decisão dura, código mais complexo.
- The Witcher 3 / Skyrim: lista categorizada por abas (armas, armaduras, consumíveis). Sem puzzle espacial, mais fácil de filtrar e ordenar.
- Terraria / Minecraft: hotbar de acesso rápido mais armazenamento massivo, com crafting amarrado ao inventário.
Não existe "o melhor". Existe o que combina com o seu jogo. Se você não tem certeza de que precisa do grid espacial, comece pela lista categorizada: é metade do trabalho e resolve 90% dos casos.
Separe em Camadas Desde o Início
O erro número um é misturar tudo no mesmo script: o item, a regra de "cabe ou não cabe" e o botão da tela vivendo juntos. Funciona no protótipo e te persegue pelo resto do projeto. Separe em camadas:
- Dados: o que é um item, o que é um slot. Sem lógica de jogo, sem UI.
- Lógica: adicionar, remover, empilhar, validar. Não sabe que UI existe.
- UI: desenha o estado e dispara ações. Não decide regra nenhuma.
- Persistência: salva e carrega.
A regra de ouro: a UI fala com a lógica, a lógica fala com os dados, e nada volta no sentido contrário escondido. Quando a lógica muda, ela avisa quem estiver ouvindo (geralmente a UI) por sinal/evento. É isso que mantém o sistema sustentável quando você adiciona crafting, comércio ou stash compartilhado depois.
A Camada de Dados
Comece definindo o item como um recurso, não como código vivo. Em Godot, Resource é perfeito pra isso: você cria cada item como um arquivo .tres no editor e referencia ele onde precisar.
# item_data.gd
class_name ItemData
extends Resource
enum Rarity { COMMON, UNCOMMON, RARE, EPIC, LEGENDARY }
@export var id: StringName
@export var display_name: String
@export var description: String
@export var icon: Texture2D
@export var rarity: Rarity = Rarity.COMMON
@export var max_stack: int = 1 # 1 = não empilha
@export var value: int = 0
@export var weight: float = 0.0
Repara que id é StringName, não String. Você vai comparar id o tempo todo (pra saber se dois itens empilham, pra salvar/carregar) e StringName compara muito mais rápido. Detalhe pequeno que importa quando o inventário tem centenas de comparações por frame.
O slot é o que carrega quantidade. Um item só, mais quantos você tem dele:
# inventory_slot.gd
class_name InventorySlot
extends RefCounted
var item: ItemData = null
var quantity: int = 0
func is_empty() -> bool:
return item == null or quantity <= 0
func can_stack(other: ItemData) -> bool:
# mesmo item e ainda tem espaço na pilha
return item != null and item.id == other.id and quantity < item.max_stack
func space_left() -> int:
if item == null:
return 0
return item.max_stack - quantity
Separar ItemData (a definição, compartilhada) de InventorySlot (a instância no inventário, com quantidade) é o detalhe que mais economiza dor depois. Cem poções no mundo apontam pro mesmo ItemData. Cada empilhamento no inventário é um slot com quantity. Você não duplica dados nem corre risco de editar a espada de um jogador e mudar a de todo mundo.
A Camada de Lógica
Aqui mora o add_item, e é onde o iniciante erra. Adicionar item não é "achar um slot vazio e jogar lá". É: primeiro tenta empilhar no que já existe, e só então usa slot vazio. Senão você acaba com três pilhas separadas de poção em vez de uma.
# inventory.gd
class_name Inventory
extends Resource
signal changed # UI escuta isso pra se redesenhar
@export var size: int = 20
var slots: Array[InventorySlot] = []
func _init() -> void:
slots.resize(size)
for i in size:
slots[i] = InventorySlot.new()
# Retorna quantos NÃO couberam (0 = adicionou tudo)
func add_item(item: ItemData, amount: int = 1) -> int:
var remaining := amount
# 1) tenta completar pilhas existentes
for slot in slots:
if remaining <= 0:
break
if slot.can_stack(item):
var add := mini(remaining, slot.space_left())
slot.quantity += add
remaining -= add
# 2) usa slots vazios pro que sobrou
for slot in slots:
if remaining <= 0:
break
if slot.is_empty():
slot.item = item
slot.quantity = mini(remaining, item.max_stack)
remaining -= slot.quantity
if remaining < amount:
changed.emit()
return remaining
func remove_item(item: ItemData, amount: int = 1) -> int:
var remaining := amount
for slot in slots:
if remaining <= 0:
break
if not slot.is_empty() and slot.item.id == item.id:
var take := mini(remaining, slot.quantity)
slot.quantity -= take
remaining -= take
if slot.quantity <= 0:
slot.item = null
if remaining < amount:
changed.emit()
return remaining
Dois pontos que valem ouro aqui. Primeiro: add_item devolve quanto não coube, em vez de só true/false. Isso te deixa fazer "pega o que cabe e larga o resto no chão" sem reescrever nada. Segundo: a lógica não conhece a UI. Ela só emite changed. Quem se atualiza é quem estiver ouvindo o sinal, e é por isso que você consegue ter duas telas (inventário e baú) olhando o mesmo dado sem gambiarra.
A versão equivalente em Unity / C# segue a mesma ideia, trocando o sinal por um evento:
// Inventory.cs
using System;
using System.Collections.Generic;
using UnityEngine;
public class Inventory : MonoBehaviour
{
[SerializeField] private int size = 20;
public event Action Changed;
private readonly List<InventorySlot> slots = new();
private void Awake()
{
for (int i = 0; i < size; i++)
slots.Add(new InventorySlot());
}
// Retorna quanto NÃO coube (0 = adicionou tudo)
public int AddItem(ItemData item, int amount = 1)
{
int remaining = amount;
foreach (var slot in slots) // completa pilhas
{
if (remaining <= 0) break;
if (slot.CanStack(item))
{
int add = Mathf.Min(remaining, slot.SpaceLeft());
slot.Quantity += add;
remaining -= add;
}
}
foreach (var slot in slots) // usa vazios
{
if (remaining <= 0) break;
if (slot.IsEmpty)
{
slot.Item = item;
slot.Quantity = Mathf.Min(remaining, item.maxStack);
remaining -= slot.Quantity;
}
}
if (remaining < amount) Changed?.Invoke();
return remaining;
}
}
Inventário em Grid (Estilo Diablo)
Se o seu jogo precisa de itens que ocupam vários slots, o modelo muda. Agora o item tem largura e altura, e antes de colocar você precisa checar se o retângulo todo está livre. É mais código, então só vá por aqui se o puzzle espacial for parte do design, não por estética.
A checagem de "cabe aqui?" é o coração:
# grid_inventory.gd
class_name GridInventory
extends Resource
@export var grid_width: int = 10
@export var grid_height: int = 6
# matriz de referências pro item (null = vazio)
var cells: Array = []
func _init() -> void:
cells.resize(grid_height)
for y in grid_height:
cells[y] = []
cells[y].resize(grid_width)
cells[y].fill(null)
func can_place(item: ItemData, origin: Vector2i, w: int, h: int) -> bool:
# passou da borda?
if origin.x + w > grid_width or origin.y + h > grid_height:
return false
if origin.x < 0 or origin.y < 0:
return false
# alguma célula da área já está ocupada?
for y in range(origin.y, origin.y + h):
for x in range(origin.x, origin.x + w):
if cells[y][x] != null:
return false
return true
func place(item: ItemData, origin: Vector2i, w: int, h: int) -> bool:
if not can_place(item, origin, w, h):
return false
for y in range(origin.y, origin.y + h):
for x in range(origin.x, origin.x + w):
cells[y][x] = item
return true
A pegadinha clássica do grid é esquecer a checagem de borda antes de varrer as células: se você só testar ocupação, um item perto da direita "vaza" pra fora da matriz e o jogo quebra com índice inválido. Por isso a borda é checada primeiro, e em separado.
UI: o Mínimo Que Faz Diferença
A interface não decide regra. Ela desenha o estado atual e, quando o jogador faz algo, chama a lógica. O ciclo é sempre: a lógica muda, emite changed, a UI ouve e se redesenha. Nunca a UI alterando o array de slots na mão.
Em Godot, um nó de inventário que escuta o sinal e refaz a tela quando algo muda:
# inventory_ui.gd
extends Control
@export var inventory: Inventory
@export var slot_scene: PackedScene # cena do slot visual
@onready var grid: GridContainer = $GridContainer
func _ready() -> void:
inventory.changed.connect(_redraw)
_build_slots()
_redraw()
func _build_slots() -> void:
for i in inventory.size:
grid.add_child(slot_scene.instantiate())
func _redraw() -> void:
for i in inventory.size:
var slot_ui = grid.get_child(i)
slot_ui.show_slot(inventory.slots[i])
Três detalhes de UX que separam o inventário "funcional" do "gostoso de usar":
- Cor por raridade. Borda branca pra comum, verde pra incomum, azul pra rara, roxo pra épica, laranja pra lendária. É a convenção que o jogador de RPG já lê de cabeça, então respeite ela. No Godot isso é um
StyleBoxpor raridade trocado no slot. - Tooltip ao passar o mouse. Nome, descrição, stats, valor. Sem tooltip o jogador fica clicando em tudo pra descobrir o que é.
- Feedback no drag. Ao arrastar um item, destaque os slots onde ele pode cair. O Godot tem
_get_drag_data,_can_drop_datae_drop_datajustamente pra isso, use o sistema nativo em vez de reinventar arrasto na mão.
::blog-cta{title="Transforme Sua Paixão em Profissão" description="Sistemas complexos como inventários são o que separam desenvolvedores iniciantes dos profissionais. Nosso programa intensivo ensina arquiteturas avançadas que estúdios AAA realmente usam." buttonText="Candidate-se Agora" icon="fas fa-rocket" variant="highlight"}::
Ordenar e Filtrar
Auto-organizar é uma daquelas funções que o jogador ama e que é simples de fazer errado. O caminho seguro: junta tudo numa lista, ordena pela chave que o jogador escolheu, limpa o inventário e re-adiciona na ordem (reusando o add_item, que já cuida do empilhamento).
# adiciona em inventory.gd
enum SortMode { NAME, VALUE_DESC, RARITY_DESC }
func sort(mode: SortMode) -> void:
# 1) coleta o que existe
var stacks: Array = []
for slot in slots:
if not slot.is_empty():
stacks.append({ "item": slot.item, "qty": slot.quantity })
# 2) ordena
match mode:
SortMode.NAME:
stacks.sort_custom(func(a, b): return a.item.display_name < b.item.display_name)
SortMode.VALUE_DESC:
stacks.sort_custom(func(a, b): return a.item.value > b.item.value)
SortMode.RARITY_DESC:
stacks.sort_custom(func(a, b): return a.item.rarity > b.item.rarity)
# 3) limpa e re-adiciona
for slot in slots:
slot.item = null
slot.quantity = 0
for s in stacks:
add_item(s.item, s.qty)
changed.emit()
Filtro e busca seguem a mesma lógica: você não altera o inventário, só decide quais slots a UI mostra. Filtragem é problema de apresentação, não de dados. Mantenha assim e você nunca corrompe o estado real ao buscar "poção".
Performance: Onde Otimizar (e Onde Não)
A maior parte dos inventários nem chega perto de ter problema de performance. Um array de 20 a 100 slots é nada pra qualquer engine. Antes de "otimizar", meça com o profiler. Dito isso, dois gargalos aparecem de verdade quando o inventário cresce:
Redesenhar a UI toda hora. Se você reconstrói todos os slots a cada item que entra, num inventário grande isso engasga. A solução é o sinal changed que já usamos: redesenha em resposta a mudança, não todo frame. Se ainda assim pesar, redesenhe só o slot que mudou em vez da grade inteira.
Recriar nós de slot. Destruir e instanciar nós de UID toda vez que abre/fecha o inventário é caro. Crie os slots uma vez e reaproveite, escondendo os que não usa. É object pooling, e pra UI de inventário rende bem porque a quantidade de slots é fixa.
O que não vale a pena cedo: cache de "peso total" e "valor total". Recalcular a soma de 50 itens é trivial. Só cacheie isso se o profiler apontar que está doendo, o que provavelmente nunca vai acontecer.
::blog-cta{title="Domine Sistemas Complexos de Game Development" description="Inventários são apenas o começo. Sistemas de combate, IA, networking - cada um requer arquiteturas específicas que você só aprende com experiência prática guiada." buttonText="Veja Nossa Grade Curricular" icon="fas fa-graduation-cap" variant="default"}::
Salvar e Carregar
Inventário sem persistência é protótipo. E aqui tem uma armadilha: não salve a referência do Resource inteiro, salve o id do item mais a quantidade. Na hora de carregar, você reconstrói a partir de um banco de itens que mapeia id pro ItemData.
Por que isso importa: se você serializa o recurso direto e depois muda a definição do item (ajusta o dano da espada, troca o ícone), os saves antigos ficam com a versão velha congelada. Salvando só id e quantidade, o save sempre puxa a definição atual. É a diferença entre um save que envelhece bem e um que vira dívida técnica.
# esboço de save: lista de { "id": ..., "qty": ... }
func to_save() -> Array:
var data: Array = []
for slot in slots:
if not slot.is_empty():
data.append({ "id": String(slot.item.id), "qty": slot.quantity })
return data
No load, você lê esse array, busca cada id no seu banco de itens e chama add_item. Simples, à prova de mudança, e nada de referência quebrada.
Qual Engine Usar Pra Isso
A arquitetura acima vale pra qualquer engine. A escolha entre Godot e Unity é mais sobre o seu jogo do que sobre o inventário:
- Godot brilha em 2D e tem o sistema de
Resourceque encaixa perfeito pra dados de item: você cria item como arquivo no editor, sem código. Pra um RPG 2D indie, é o caminho mais rápido. Curva de entrada suave, gratuito, export pra Steam tranquilo. - Unity tem
ScriptableObject, que cumpre o mesmo papel doResource, e ganha em maturidade de mercado mobile, integrações e Asset Store. Se o destino é mobile ou você já tem time em C#, faz sentido.
Se é seu primeiro jogo e a dúvida persiste, vá de Godot. Você aprende os conceitos (dados, lógica, sinal, UI) que migram pra qualquer engine depois, sem o peso de uma ferramenta maior do que o projeto precisa.
Por Onde Começar
Não tente construir tudo de uma vez. A ordem que funciona:
ItemDataeInventorySlot. Os dados primeiro, sem UI nenhuma. Teste no console queadd_itemempilha certo.add_item/remove_itemcom empilhamento. Garanta que poção junta com poção antes de desenhar qualquer coisa.- UI básica. Slots na tela ouvindo o sinal
changed. - Polish. Drag and drop, tooltip, cor por raridade.
- Ordenar, filtrar e salvar. Por último, porque dependem de tudo acima estar firme.
O inventário é uma das telas que o jogador mais usa no seu RPG. Se a base estiver bem separada em camadas, adicionar crafting, comércio e baú depois é incremento, não reescrita. Comece simples, teste cada pedaço, e cresça em cima de uma fundação que você entende.


