Voltar para o Blog
Quest Log

Drag and Drop no Godot: Arrastar Itens de Inventário do Jeito Nativo

Cursor arrastando o ícone de uma espada entre slots de um inventário de RPG

Aprenda drag and drop godot do jeito nativo: _get_drag_data, _can_drop_data e _drop_data para arrastar itens de inventário, trocar slots e validar tipos.

Drag and Drop no Godot: Arrastar Itens de Inventário do Jeito Nativo

A primeira vez que eu implementei drag and drop num inventário, fiz tudo na mão: detectar clique, criar um sprite que segue o mouse, testar colisão com cada slot no soltar, tratar o caso de soltar fora da tela. Funcionava, mas era um castelo de cartas. Depois descobri que o sistema de drag and drop godot já vem pronto dentro de todo node Control, com três métodos virtuais que resolvem o ciclo inteiro: _get_drag_data, _can_drop_data e _drop_data. Você não escreve uma linha de "seguir o mouse".

Esse tutorial monta o caso clássico: uma grade de slots de inventário onde o jogador arrasta itens entre slots, troca dois itens de lugar quando o destino está ocupado, e slots de equipamento que só aceitam o tipo certo (arma no slot de arma, capacete no slot de cabeça). Todo código é GDScript do Godot 4.x. Se você ainda não tem a estrutura de dados do inventário, vale ler antes como criar um sistema de inventário, porque aqui o foco é a camada de UI.

Como o sistema nativo funciona

O ciclo tem três momentos, e cada um vive num método virtual de Control:

  • _get_drag_data(at_position): chamado no node onde o arrasto começa, quando o Godot detecta que o jogador clicou e moveu. Você retorna um Variant qualquer (um dicionário, um Resource, o que fizer sentido). Se retornar null, não tem arrasto.
  • _can_drop_data(at_position, data): chamado no node que está embaixo do mouse enquanto o arrasto acontece. Retorna true ou false. É aqui que o cursor muda pra indicar se o drop é válido, e o Godot chama isso todo frame de movimento, então mantenha barato.
  • _drop_data(at_position, data): chamado no node de destino quando o jogador solta, mas só se o _can_drop_data daquele node retornou true. É onde o estado muda de verdade.

O detalhe que faz tudo encaixar: o data que sai do _get_drag_data é exatamente o mesmo objeto que chega nos outros dois métodos. O Godot só transporta. Você decide o formato, e a minha recomendação é um dicionário com referência ao slot de origem, porque sem ela não dá pra fazer troca nem limpar o slot antigo.

Estrutura: um slot que é um Control

Cada slot do inventário é uma cena pequena. Um PanelContainer com uma TextureRect dentro pro ícone resolve:

InventorySlot (PanelContainer)
└── Icon (TextureRect)

O dado do item pode ser um Resource simples:

class_name ItemData
extends Resource

@export var nome: String
@export var icone: Texture2D
@export var tipo: String = "geral"  # "arma", "capacete", "consumivel"...

E o script do slot começa guardando o item e atualizando o ícone:

class_name InventorySlot
extends PanelContainer

@export var item: ItemData

@onready var icon: TextureRect = $Icon

func _ready():
    _atualizar_icone()

func set_item(novo_item: ItemData) -> void:
    item = novo_item
    _atualizar_icone()

func _atualizar_icone():
    if item:
        icon.texture = item.icone
    else:
        icon.texture = null

Uma armadilha antes de qualquer código de drag: o mouse_filter. O slot (PanelContainer) precisa estar em Stop ou Pass pra receber eventos, e a TextureRect do ícone deve ficar em Ignore, senão ela rouba o evento do pai e os métodos virtuais do slot nunca são chamados. Esse é o motivo número um de "meu drag and drop não dispara" que eu vejo em aluno.

Iniciando o arrasto com _get_drag_data

No mesmo script do slot:

func _get_drag_data(at_position: Vector2) -> Variant:
    if item == null:
        return null

    var preview = TextureRect.new()
    preview.texture = item.icone
    preview.custom_minimum_size = Vector2(64, 64)
    preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
    set_drag_preview(preview)

    return {"item": item, "slot_origem": self}

Duas coisas acontecendo aqui. Primeiro, slot vazio retorna null e o arrasto nem começa, sem precisar de nenhum if em outro lugar. Segundo, o set_drag_preview recebe um Control que vai seguir o mouse durante o arrasto inteiro. O Godot cuida do posicionamento e da destruição desse preview sozinho, então você cria o node, entrega, e esquece. Não chame add_child nele, o set_drag_preview já faz isso internamente.

O retorno é o dicionário com o item e o slot de origem. Guarde essa decisão: é ela que vai permitir a troca de itens daqui a pouco.

Validando o destino com _can_drop_data

A versão mínima aceita qualquer arrasto que tenha cara de item:

func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
    return data is Dictionary and data.has("item")

Esse retorno controla o feedback visual do cursor: sobre um slot que retorna true, o cursor indica drop permitido; sobre qualquer área que retorna false (ou que nem implementa o método), indica proibido. De graça, sem você desenhar nada.

Se quiser um destaque mais forte, dá pra acender o slot enquanto um arrasto válido paira sobre ele, usando os sinais mouse_entered e mouse_exited junto com get_viewport().gui_is_dragging(), que diz se existe um arrasto em andamento. Mas pra primeira versão, o cursor já comunica o essencial.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Soltando e trocando itens com _drop_data

Aqui entra a lógica de verdade, e é onde o slot_origem que guardamos paga a passagem:

func _drop_data(at_position: Vector2, data: Variant) -> void:
    var slot_origem: InventorySlot = data["slot_origem"]

    if slot_origem == self:
        return

    var item_que_chega: ItemData = data["item"]
    var item_que_estava: ItemData = item

    set_item(item_que_chega)
    slot_origem.set_item(item_que_estava)

Leia com calma porque esse trecho cobre os dois cenários de uma vez. Se o slot de destino estava vazio, item_que_estava é null: o destino recebe o item e a origem recebe null, ou seja, o item foi movido. Se o destino estava ocupado, os dois set_item fazem a troca: o item de lá vai pra cá, o de cá vai pra lá. Swap de inventário em quatro linhas, sem caso especial.

O if slot_origem == self evita o jogador soltar o item no próprio slot e disparar lógica à toa. E repare que o _drop_data só roda se o _can_drop_data aprovou, então aqui dentro você pode confiar no formato do data sem revalidar tudo.

Um efeito colateral bom desse desenho: soltar o item fora de qualquer slot simplesmente não faz nada. Nenhum _drop_data é chamado, o preview some, o item continua na origem. O comportamento padrão já é o comportamento seguro.

Slot de equipamento: validar tipo no _can_drop_data

Agora o requisito de RPG: o slot de arma só aceita arma. Em vez de duplicar a cena, estenda o slot com um filtro:

class_name EquipmentSlot
extends InventorySlot

@export var tipo_aceito: String = "arma"

func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
    if not (data is Dictionary and data.has("item")):
        return false
    var item_arrastado: ItemData = data["item"]
    return item_arrastado.tipo == tipo_aceito

Só isso. O _drop_data herdado continua funcionando, porque a troca não muda; o que muda é quem pode entrar. Arraste uma poção sobre o slot de capacete e o cursor já mostra o proibido antes mesmo de soltar, sem você escrever uma mensagem de erro sequer. Esse é o ganho real do sistema nativo: a validação mora num lugar só e o feedback vem junto.

Tem um refinamento que vale pensar: na troca envolvendo slot de equipamento, o item que volta pra origem também precisa ser válido lá. Entre slots genéricos de mochila isso nunca falha, mas se você permitir arrastar do slot de arma direto pro slot de anel, a troca devolveria uma arma pra um slot de anel. A solução simples é validar a volta dentro do _drop_data antes de trocar, e recusar em silêncio se não couber. Em jogos com loja, o mesmo dicionário de drag serve pra arrastar item da loja pro inventário; a lógica de compra entra no _drop_data do lado do jogador, como detalho em sistema de loja para o seu jogo.

Erros comuns que travam o drag and drop

mouse_filter errado. Já falei, mas repito porque é o campeão: filho com Stop engole o evento e o _get_drag_data do pai nunca roda. Ícones e labels dentro do slot ficam em Ignore.

Esquecer o return null pra slot vazio. Sem ele, arrastar do slot vazio cria um drag com item nulo e o crash aparece longe, no _drop_data de outro slot. Corte na origem.

Preview com add_child manual. O set_drag_preview é dono do preview. Se você adicionar o node na cena por conta própria, ganha um ícone fantasma que não some no fim do arrasto.

Lógica pesada no _can_drop_data. Ele roda a cada movimento do mouse durante o arrasto. Comparar um campo tipo é instantâneo; varrer o inventário inteiro ou instanciar coisas ali derruba o frame rate só de segurar um item.

Mexer no estado no _get_drag_data. Não remova o item da origem quando o arrasto começa. Se o jogador soltar fora de um destino válido, não existe callback de "cancelou", e o item evapora. O estado só muda no _drop_data, que é o único ponto com garantia de sucesso.

Fechando

Drag and drop no Godot é um contrato de três métodos: _get_drag_data decide o que viaja e mostra o preview, _can_drop_data decide quem pode receber, _drop_data efetiva a mudança. Guardando o slot de origem dentro do data, mover e trocar itens viram o mesmo código, e validar tipo de slot é uma sobrescrita de um método na classe filha.

Pra fixar, monta uma GridContainer com uns doze slots de mochila, dois slots de equipamento com tipo_aceito diferente, e três ou quatro ItemData de teste. Em meia hora você tem o esqueleto de inventário de qualquer RPG, e o melhor: sem nenhum código rastreando posição de mouse, porque essa parte nunca foi sua responsabilidade.