Arrastar e soltar (drag and drop) no Godot 4: guia prático

Aprenda a implementar arrastar e soltar (drag and drop) no Godot 4 de duas formas: com nós Control para UI e inventário, e arrastando nós 2D no mundo.
Arrastar e soltar (drag and drop) no Godot é um daqueles recursos que parecem simples até você tentar montar do zero. A boa notícia é que o Godot 4 já entrega quase tudo pronto para interface, e o resto você resolve com pouquíssimo código. Neste tutorial você vai implementar arrastar e soltar de duas formas diferentes: uma para UI, inventário e cartas usando nós Control, e outra para objetos livres no mundo, como peças de puzzle que seguem o mouse. Vou mostrar quando cada abordagem faz sentido e deixar os exemplos prontos para você testar no editor.
As duas abordagens: quando usar cada uma
Antes de escrever código, entenda a decisão. O Godot tem dois caminhos bem distintos para arrastar coisas, e escolher errado dá muito trabalho à toa.
O primeiro caminho é o sistema nativo de drag and drop dos nós Control. Ele funciona com três métodos virtuais que você sobrescreve: _get_drag_data, _can_drop_data e _drop_data. Esse sistema entende o conceito de "origem" e "destino": você arrasta de um lugar e solta em outro, e o Godot cuida da prévia visual e de detectar sobre qual nó o mouse está. É o caminho certo para inventário, barra de itens, cartas indo para uma mesa, arrastar arquivos de uma lista para outra. Sempre que a ideia é "pegar deste slot e largar naquele slot", use Control.
O segundo caminho é mover um nó 2D livre pelo mundo, seguindo o mouse com _input e get_global_mouse_position. Aqui não existe slot: o objeto pode parar em qualquer coordenada. É o caminho certo para peças de quebra-cabeça, tokens de tabuleiro, objetos físicos que você reposiciona. Você controla tudo na mão, o que dá mais liberdade e mais responsabilidade.
A regra prática: se o destino é discreto (slots, listas, áreas), vá de Control. Se o destino é contínuo (qualquer ponto da tela), vá de nó 2D.
Abordagem 1: drag and drop com nós Control
Vamos montar um inventário simples. Imagine vários slots (nós Control) e itens dentro deles. Ao arrastar um item de um slot para outro, o item troca de lugar. Toda a mágica acontece nos três métodos virtuais.
O primeiro é o _get_drag_data. O Godot chama esse método quando o jogador começa a arrastar a partir do Control. O que você retornar vira o "pacote" que viaja com o mouse. Retorne null para dizer que aquele nó não pode ser arrastado.
extends Panel
class_name SlotInventario
var item_atual: String = ""
func _get_drag_data(at_position: Vector2) -> Variant:
if item_atual == "":
return null
# Monta o pacote de dados que viaja com o arraste
var dados: Dictionary = {
"tipo": "item_inventario",
"item": item_atual,
"origem": self
}
# Cria uma prévia visual que segue o mouse
var previa: TextureRect = TextureRect.new()
previa.texture = $Icone.texture
previa.custom_minimum_size = Vector2(48, 48)
set_drag_preview(previa)
return dados
Repare no tipo de retorno Variant. É obrigatório, porque o método pode retornar tanto um Dictionary quanto null. O set_drag_preview recebe um Control qualquer e o exibe grudado no cursor durante o arraste. Sem ele o arraste funciona, mas o jogador não vê nada se mexendo.
O segundo método é o _can_drop_data. O Godot chama ele constantemente enquanto o mouse paira sobre o Control durante um arraste. Você devolve true se aquele nó aceita o dado, ou false se recusa. É aqui que você valida regras: só aceitar itens do tipo certo, bloquear slots ocupados, o que fizer sentido para o jogo.
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
# Só aceita dicionários marcados como item de inventário
if typeof(data) != TYPE_DICTIONARY:
return false
return data.get("tipo", "") == "item_inventario"
O terceiro método é o _drop_data. Ele só dispara quando o jogador solta o botão E o _can_drop_data retornou true para aquele nó. É onde a troca realmente acontece.
func _drop_data(at_position: Vector2, data: Variant) -> void:
var origem: SlotInventario = data["origem"]
var item_recebido: String = data["item"]
# Troca os itens entre o slot de origem e este slot
var item_temporario: String = item_atual
definir_item(item_recebido)
origem.definir_item(item_temporario)
func definir_item(novo_item: String) -> void:
item_atual = novo_item
$Icone.texture = load("res://itens/%s.png" % novo_item) if novo_item != "" else null
Um detalhe que derruba muita gente: o Control só participa do drag and drop se receber eventos de mouse. Confira o mouse_filter no Inspetor. Se estiver como Ignore, o nó fica invisível para o arraste. Use Stop ou Pass. Esse é o motivo número um de _drop_data que nunca é chamado.
Esse mesmo trio de métodos é a espinha dorsal de cartas arrastáveis. Se você está construindo um jogo de baralho, a lógica de pegar uma carta da mão e soltar numa zona de mesa é idêntica: _get_drag_data na carta, _can_drop_data e _drop_data na zona. Para ver isso aplicado num projeto completo, vale conferir o passo a passo de como fazer um jogo deck builder, onde o arraste de cartas é peça central. E se o seu foco é RPG, esse sistema de slots é a base de um sistema de inventário para RPG que aguenta equipar, empilhar e mover itens.
Abordagem 2: arrastar um nó 2D pelo mundo
Agora o caso do puzzle. Aqui não temos slots: a peça precisa seguir o mouse livremente e parar onde o jogador soltar. Vamos usar um Node2D (ou um Sprite2D) com uma Area2D para detectar o clique, e controlar tudo com uma flag booleana.
A ideia é: quando o jogador aperta o botão esquerdo sobre a peça, ligamos arrastando = true. Enquanto essa flag estiver ligada, a posição da peça acompanha get_global_mouse_position. Quando o jogador solta, desligamos a flag.
extends Area2D
class_name PecaPuzzle
var arrastando: bool = false
var offset: Vector2 = Vector2.ZERO
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed and _mouse_sobre_peca():
arrastando = true
# Guarda a distância entre o mouse e o centro da peça
offset = global_position - get_global_mouse_position()
elif not event.pressed:
arrastando = false
func _process(delta: float) -> void:
if arrastando:
global_position = get_global_mouse_position() + offset
func _mouse_sobre_peca() -> bool:
# Checa se o cursor está dentro do CollisionShape da Area2D
var pos_local: Vector2 = to_local(get_global_mouse_position())
return get_node("CollisionShape2D").shape.get_rect().has_point(pos_local)
Esse offset é o segredo de um arraste que parece profissional. Sem ele, a peça "pula" para centralizar no cursor no instante do clique. Guardando a diferença entre o ponto clicado e o centro da peça, ela continua exatamente onde o jogador pegou, sem saltos.
Uma versão mais robusta detecta o clique pelo próprio sinal input_event da Area2D, em vez de calcular o retângulo na mão:
func _ready() -> void:
input_event.connect(_ao_receber_input)
func _ao_receber_input(viewport: Node, event: InputEvent, shape_idx: int) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
arrastando = true
offset = global_position - get_global_mouse_position()
Com o sinal input_event, o Godot já te avisa quando o clique caiu dentro da forma de colisão da peça, então você não precisa testar o ponto manualmente. Continue desligando a flag no _input global, porque o jogador pode soltar o botão fora da peça.
Para encaixar a peça num lugar certo ao soltar, compare a posição final com a posição alvo e faça um "snap" se estiver perto o suficiente:
@export var posicao_alvo: Vector2 = Vector2.ZERO
@export var distancia_encaixe: float = 32.0
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if not event.pressed and arrastando:
arrastando = false
if global_position.distance_to(posicao_alvo) < distancia_encaixe:
global_position = posicao_alvo
Esse encaixe por proximidade é o coração de muitos quebra-cabeças. Se você quer entender como transformar essas mecânicas em desafios que prendem o jogador, dá uma olhada nas ideias de design de puzzle, porque código de arraste é só metade do trabalho.
Comparando as duas abordagens na prática
Vale reforçar as diferenças com um resumo direto.
O sistema de Control cuida sozinho da detecção de destino, da prévia visual e de saber quando o drop é válido. Você escreve pouco e ganha comportamento consistente com o resto da UI. A limitação é que ele vive no mundo dos Controls, com slots e áreas definidas, e não combina bem com física ou coordenadas livres.
O arraste de nó 2D te dá controle total da posição, funciona com física, colisão e snap personalizado, e não depende de slots. O preço é que você mesmo escreve a detecção de clique, o acompanhamento do mouse e a lógica de soltar. Nada complicado, mas é tudo por sua conta.
Na dúvida, comece perguntando onde o item pode parar. Destino fixo e discreto pede Control. Destino livre e contínuo pede nó 2D. Muitos jogos usam as duas coisas ao mesmo tempo: um inventário de Control na lateral e peças 2D arrastáveis no tabuleiro.
Erros comuns e como evitar
Alguns tropeços aparecem sempre que alguém implementa isso pela primeira vez. Fica a lista para você conferir.
O _drop_data não dispara: quase sempre é _can_drop_data devolvendo false sem querer, ou mouse_filter em Ignore no nó de destino. Teste os dois métodos juntos e verifique o filtro no Inspetor.
A prévia não aparece: você esqueceu do set_drag_preview dentro do _get_drag_data, ou passou um Control sem tamanho definido. Dê um custom_minimum_size para a prévia.
A peça 2D pula ao clicar: faltou o offset. Guarde global_position - get_global_mouse_position() no momento do clique e some de volta a cada frame.
A peça continua presa ao mouse depois de soltar: você só ligou a flag e nunca desligou. Garanta o ramo elif not event.pressed para zerar arrastando, e faça isso no _input global, não só sobre a peça.
Com esses dois padrões no bolso, você cobre praticamente todos os casos de arrastar e soltar (drag and drop) no Godot 4, de inventários e cartas até puzzles com encaixe. Copie os exemplos, rode no editor e ajuste os tipos de dado para o seu jogo. O sistema é sólido e, uma vez montado, você reaproveita em projeto atrás de projeto.
Perguntas frequentes
Qual a diferença entre os dois métodos de arrastar e soltar no Godot?
O sistema de nós Control usa os métodos virtuais _get_drag_data, _can_drop_data e _drop_data e serve para interface, inventário e cartas, onde você solta em slots. Arrastar um nó 2D com _input e get_global_mouse_position é para objetos livres no mundo, como peças de puzzle que seguem o mouse em qualquer posição.
O que a função _get_drag_data precisa retornar?
Ela retorna os dados que serão carregados durante o arraste, normalmente um Dictionary com o tipo do item e a referência de origem. Se retornar null, o Godot entende que aquele Control não pode iniciar um arraste. Você também pode chamar set_drag_preview para mostrar uma prévia visual seguindo o mouse.
Por que meu _drop_data nunca é chamado?
Na maioria das vezes é porque _can_drop_data retornou false, ou porque o mouse_filter do Control alvo está como MOUSE_FILTER_IGNORE. O _drop_data só dispara quando _can_drop_data aprova o dado sobre aquele nó, então valide os dois métodos juntos.
Preciso de mouse_filter para o drag and drop de Control funcionar?
Sim. O Control precisa receber eventos de mouse, então use MOUSE_FILTER_STOP ou MOUSE_FILTER_PASS. Com MOUSE_FILTER_IGNORE o nó não recebe o arraste e nem inicia nem aceita drops.
Como faço a peça parar de seguir o mouse ao soltar o botão?
No método _input, escute o InputEventMouseButton. Quando o botão esquerdo for pressionado sobre a peça, ligue uma flag arrastando como true, e quando for solto, volte para false. Enquanto a flag estiver true, atualize a posição com get_global_mouse_position dentro do _input ou _process.


