Remapear Controles na Godot 4: Rebind em Tempo Real e Salvar em ConfigFile

Aprenda a remapear controles Godot em tempo de execução: capturar a tecla, gravar no InputMap, tratar conflito, restaurar padrão e salvar em ConfigFile.
Deixar o jogador remapear controles Godot em tempo de execução não é luxo, é acessibilidade básica. Teclados variam de layout, canhotos preferem outras teclas, jogadores com mobilidade reduzida precisam de posições confortáveis, e muita gente simplesmente odeia o padrão WASD e quer setas, ou o contrário. Um jogo que trava o input em teclas fixas exclui pessoas sem necessidade. Neste tutorial você vai montar uma tela de opções que captura a tecla nova enquanto o jogo roda, grava no InputMap, resolve conflitos de tecla, oferece restaurar padrão e persiste tudo em um ConfigFile. Nada de recompilar, nada de mexer no editor: o jogador troca e continua jogando.
Este texto assume que você já sabe declarar as ações. Se ainda está montando o esqueleto, veja antes como configurar as ações de input no editor da Godot e volte aqui para a parte de deixar o jogador trocar as teclas rodando o jogo.
Por que rebind em runtime é diferente do InputMap do editor
No editor, você abre Project Settings, cria ações como mover_esquerda, pular, atacar e associa teclas. Isso vira o mapeamento padrão embutido no jogo. O problema: essas associações são estáticas. O jogador não abre o editor da Godot.
A boa notícia é que o InputMap é totalmente manipulável em GDScript. As mesmas ações que você criou no editor podem ter seus eventos apagados e reescritos em tempo de execução com duas chamadas:
InputMap.action_erase_events("pular")remove todos os eventos associados à ação.InputMap.action_add_event("pular", evento)adiciona um novo evento de input à ação.
O evento é um objeto como InputEventKey (teclado) ou InputEventJoypadButton (controle). O resto do jogo continua chamando Input.is_action_pressed("pular") sem saber que a tecla mudou. É essa separação entre a ação (nome lógico) e o evento (tecla física) que torna o rebind possível e limpo.
O fluxo do rebind, passo a passo
O comportamento que vamos construir é o clássico das telas de opções:
- O jogador clica no botão da ação que quer trocar (por exemplo, o botão ao lado de "Pular").
- O botão entra em estado de escuta e mostra algo como "Aguardando tecla...".
- O próximo input que chegar é capturado em
_input(). - Verificamos conflito com outras ações.
- Gravamos o novo evento no InputMap e atualizamos o texto do botão.
- Persistimos o mapa inteiro no ConfigFile.
Vamos por partes.
Montando a linha de rebind
Cada linha da tela precisa saber qual ação ela representa e ter um botão que dispara a escuta. Uma cena reutilizável resolve isso. Suponha uma cena RebindRow com um Label para o nome amigável e um Button para a tecla atual.
extends HBoxContainer
signal rebind_solicitado(acao: StringName, linha: Node)
@export var acao: StringName = &""
@export var nome_amigavel: String = ""
@onready var label_nome: Label = $LabelNome
@onready var botao_tecla: Button = $BotaoTecla
func _ready() -> void:
label_nome.text = nome_amigavel
atualizar_texto()
botao_tecla.pressed.connect(_ao_clicar)
func _ao_clicar() -> void:
botao_tecla.text = "Aguardando tecla..."
rebind_solicitado.emit(acao, self)
func atualizar_texto() -> void:
var eventos: Array[InputEvent] = InputMap.action_get_events(acao)
if eventos.is_empty():
botao_tecla.text = "Sem tecla"
return
botao_tecla.text = _nome_do_evento(eventos[0])
func _nome_do_evento(evento: InputEvent) -> String:
if evento is InputEventKey:
var tecla: InputEventKey = evento
return OS.get_keycode_string(tecla.physical_keycode)
if evento is InputEventJoypadButton:
var botao: InputEventJoypadButton = evento
return "Botão %d" % botao.button_index
return evento.as_text()
Repare que uso physical_keycode para o teclado. Isso é importante: physical_keycode representa a posição física da tecla, então a letra "W" fica na mesma posição num teclado QWERTY ou AZERTY. Para um jogo, a posição costuma importar mais que o rótulo impresso na tecla.
Capturando a próxima tecla em _input()
O controlador da tela guarda qual ação está em escuta. Enquanto houver uma ação aguardando, _input() intercepta o próximo evento válido.
extends Control
var acao_em_escuta: StringName = &""
var linha_em_escuta: Node = null
func _ready() -> void:
for linha in $ListaRebind.get_children():
if linha.has_signal("rebind_solicitado"):
linha.rebind_solicitado.connect(_iniciar_escuta)
func _iniciar_escuta(acao: StringName, linha: Node) -> void:
acao_em_escuta = acao
linha_em_escuta = linha
set_process_input(true)
func _input(evento: InputEvent) -> void:
if acao_em_escuta == &"":
return
# Cancelar com ESC sem trocar nada.
if evento is InputEventKey and evento.pressed and evento.keycode == KEY_ESCAPE:
_finalizar_escuta()
return
if not _evento_valido(evento):
return
_aplicar_rebind(acao_em_escuta, evento)
_finalizar_escuta()
get_viewport().set_input_as_handled()
func _evento_valido(evento: InputEvent) -> bool:
if evento is InputEventKey:
return evento.pressed and not evento.echo
if evento is InputEventJoypadButton:
return evento.pressed
return false
Só aceitamos eventos "de pressionar" (pressed) e ignoramos repetição de tecla (echo), para não capturar lixo. O set_input_as_handled() evita que a mesma tecla dispare outra ação da interface no mesmo frame.
Tratando conflito de tecla
Se o jogador coloca "Espaço" em Pular, mas "Espaço" já é o Atacar, você precisa de uma política. Duas são comuns:
- Recusar a troca: avisa que a tecla já está em uso e não muda nada.
- Roubar a tecla: remove a tecla da ação antiga e a coloca na nova, deixando a antiga sem bind até o jogador resolver.
Vou mostrar a versão que detecta o conflito e deixa você decidir. A função varre as ações procurando quem usa a mesma tecla física.
func _acao_em_conflito(evento: InputEvent, acao_alvo: StringName) -> StringName:
for acao in InputMap.get_actions():
if acao == acao_alvo:
continue
# Ignora ações internas da engine (prefixo ui_) se quiser.
if String(acao).begins_with("ui_"):
continue
for existente in InputMap.action_get_events(acao):
if _mesmo_evento(evento, existente):
return acao
return &""
func _mesmo_evento(a: InputEvent, b: InputEvent) -> bool:
if a is InputEventKey and b is InputEventKey:
return a.physical_keycode == b.physical_keycode
if a is InputEventJoypadButton and b is InputEventJoypadButton:
return a.button_index == b.button_index
return false
Agora o _aplicar_rebind usa isso. Aqui escolhi a política de "roubar a tecla": limpo a tecla da ação conflitante antes de gravar na nova. Se preferir recusar, basta retornar cedo com uma mensagem.
func _aplicar_rebind(acao: StringName, evento: InputEvent) -> void:
var conflito: StringName = _acao_em_conflito(evento, acao)
if conflito != &"":
# Política: remover a tecla da ação antiga.
_remover_evento_da_acao(conflito, evento)
_atualizar_linha_da_acao(conflito)
InputMap.action_erase_events(acao)
InputMap.action_add_event(acao, evento)
if linha_em_escuta and linha_em_escuta.has_method("atualizar_texto"):
linha_em_escuta.atualizar_texto()
salvar_controles()
func _remover_evento_da_acao(acao: StringName, evento: InputEvent) -> void:
for existente in InputMap.action_get_events(acao):
if _mesmo_evento(evento, existente):
InputMap.action_erase_event(acao, existente)
func _atualizar_linha_da_acao(acao: StringName) -> void:
for linha in $ListaRebind.get_children():
if linha.get("acao") == acao and linha.has_method("atualizar_texto"):
linha.atualizar_texto()
func _finalizar_escuta() -> void:
acao_em_escuta = &""
if linha_em_escuta and linha_em_escuta.has_method("atualizar_texto"):
linha_em_escuta.atualizar_texto()
linha_em_escuta = null
set_process_input(false)
Note que uso action_erase_events (plural) na ação que está recebendo a tecla nova, porque geralmente queremos uma tecla por ação nessa tela. Se o seu jogo aceita várias teclas para a mesma ação, adapte para só adicionar sem apagar.
Salvando o mapeamento em ConfigFile
Trocar as teclas em runtime não adianta se tudo volta ao padrão quando o jogo fecha. O ConfigFile é a forma direta de persistir isso em user://, que é a pasta de dados do jogador (fora do executável, segura para escrita).
A ideia: para cada ação customizável, gravamos uma representação do evento. Vou usar um par de campos por ação, tipo e código, que é fácil de reconstruir.
const CAMINHO_CONFIG: String = "user://input.cfg"
# Lista das ações que o jogador pode remapear.
const ACOES_CUSTOMIZAVEIS: Array[StringName] = [
&"mover_esquerda", &"mover_direita", &"pular", &"atacar", &"interagir",
]
func salvar_controles() -> void:
var config: ConfigFile = ConfigFile.new()
for acao in ACOES_CUSTOMIZAVEIS:
var eventos: Array[InputEvent] = InputMap.action_get_events(acao)
if eventos.is_empty():
continue
var evento: InputEvent = eventos[0]
if evento is InputEventKey:
config.set_value(acao, "tipo", "tecla")
config.set_value(acao, "codigo", evento.physical_keycode)
elif evento is InputEventJoypadButton:
config.set_value(acao, "tipo", "joypad")
config.set_value(acao, "codigo", evento.button_index)
config.save(CAMINHO_CONFIG)
E o carregamento, que precisa rodar na inicialização do jogo, antes da primeira cena jogável. Um bom lugar é um autoload (singleton) de configurações.
func carregar_controles() -> void:
var config: ConfigFile = ConfigFile.new()
var erro: int = config.load(CAMINHO_CONFIG)
if erro != OK:
return # Sem arquivo salvo: mantém o padrão do projeto.
for acao in ACOES_CUSTOMIZAVEIS:
if not config.has_section(acao):
continue
var tipo: String = config.get_value(acao, "tipo", "")
var codigo: int = config.get_value(acao, "codigo", 0)
var evento: InputEvent = _reconstruir_evento(tipo, codigo)
if evento == null:
continue
InputMap.action_erase_events(acao)
InputMap.action_add_event(acao, evento)
func _reconstruir_evento(tipo: String, codigo: int) -> InputEvent:
if tipo == "tecla":
var tecla: InputEventKey = InputEventKey.new()
tecla.physical_keycode = codigo
return tecla
if tipo == "joypad":
var botao: InputEventJoypadButton = InputEventJoypadButton.new()
botao.button_index = codigo
return botao
return null
Chame carregar_controles() no _ready() do autoload. Assim, quando o jogador abre o jogo, o mapa salvo é reaplicado antes de qualquer gameplay. A tela de rebind, por sua vez, sempre lê o estado atual do InputMap com action_get_events, então ela reflete o que foi carregado sem código extra.
Guardando o padrão para restaurar depois
Antes de aplicar qualquer coisa salva, vale capturar o mapeamento original. A Godot 4 oferece um atalho: InputMap.load_from_project_settings() reseta o InputMap para o que está definido no projeto. Combinado com apagar o arquivo salvo, isso dá o botão restaurar padrão de graça.
func restaurar_padrao() -> void:
# Volta o InputMap ao definido em Project Settings.
InputMap.load_from_project_settings()
# Remove o arquivo salvo para não sobrescrever no próximo boot.
if FileAccess.file_exists(CAMINHO_CONFIG):
DirAccess.remove_absolute(CAMINHO_CONFIG)
# Atualiza todos os botões da tela.
for linha in $ListaRebind.get_children():
if linha.has_method("atualizar_texto"):
linha.atualizar_texto()
Ligue essa função ao botão "Restaurar padrão" da sua tela de opções. Simples e sem manter uma cópia manual do mapa.
Encaixando na tela de opções
Toda essa lógica vive dentro da sua tela de opções. Se você ainda não tem esse menu, o tutorial de como criar o menu principal e a tela de opções na Godot mostra a estrutura de navegação onde a aba de controles vai morar. A recomendação é ter uma cena de opções com abas (áudio, vídeo, controles) e colocar a ListaRebind na aba de controles.
Do ponto de vista de acessibilidade, o rebind é só o começo. Vale conferir também nosso guia de design inclusivo no desenvolvimento de jogos acessíveis, que cobre por que dar controle sobre o input, tamanho de fonte e contraste faz diferença real para quem joga. Uma tela de rebind bem feita já resolve uma parte grande das barreiras de entrada.
Um detalhe de polimento: sempre mostre a tecla atual no botão, nunca deixe o jogador adivinhar. E ofereça um jeito de cancelar a escuta (o ESC no exemplo) para quem clicou por engano não ficar preso.
Checklist final
Antes de fechar a feature, confira:
- As ações que você quer permitir remapear estão em
ACOES_CUSTOMIZAVEIS. carregar_controles()roda no autoload, antes da primeira cena.- O rebind trata conflito com uma política clara e consistente.
- O botão restaurar padrão apaga o arquivo e recarrega do projeto.
- ESC (ou outro botão) cancela a escuta sem quebrar nada.
- O texto do botão sempre reflete a tecla atual.
Conclusão
Remapear controles Godot em runtime é um bloco de código pequeno com impacto grande na experiência. Com InputMap.action_erase_events, action_add_event, uma captura em _input() e um ConfigFile em user://, você entrega uma tela de opções que respeita o jeito de cada jogador jogar, incluindo quem depende disso para conseguir jogar. Detecção de conflito e restaurar padrão fecham as arestas.
Se você está montando o sistema de input do seu jogo do zero e quer ir mais fundo em arquitetura, ações compostas e boas práticas de gameplay, dá uma olhada nos outros tutoriais de Godot aqui no CursoGame.Dev e siga construindo a partir daqui.
Perguntas frequentes
Como capturar a próxima tecla que o jogador apertar na Godot?
Coloque a tela de rebind em modo de escuta, marque qual ação está sendo remapeada e leia o evento em _input(). Quando chegar um InputEventKey ou InputEventJoypadButton válido, use InputMap.action_erase_events para limpar a ação e InputMap.action_add_event para gravar o novo evento.
Como salvar o remapeamento de controles entre sessões?
Use um ConfigFile gravado em user://input.cfg. Para cada ação, serialize os eventos com as_text() ou guarde o keycode e o tipo de dispositivo. Na inicialização do jogo, carregue o arquivo e reaplique os eventos no InputMap antes da primeira cena jogável.
Como evitar que duas ações usem a mesma tecla?
Antes de gravar o novo evento, percorra as ações do InputMap procurando quem já usa aquela tecla. Se achar conflito, você pode recusar a troca e avisar o jogador, ou remover a tecla da ação antiga para deixá-la sem bind. Escolha uma política e mantenha ela consistente na tela.
Como restaurar os controles padrão na Godot?
Guarde o mapeamento original assim que o jogo abre, antes de aplicar qualquer customização salva. O botão restaurar padrão apaga o arquivo em user://, recarrega o InputMap a partir do projeto com InputMap.load_from_project_settings() e atualiza a interface.


