Voltar para o Blog
Quest Log

Remapear Controles na Godot 4: Rebind e Salvar em ConfigFile

Tela de opções de um jogo na Godot mostrando uma lista de ações com botões para trocar as teclas

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.

Ferramenta:Godot

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:

  1. O jogador clica no botão da ação que quer trocar (por exemplo, o botão ao lado de "Pular").
  2. O botão entra em estado de escuta e mostra algo como "Aguardando tecla...".
  3. O próximo input que chegar é capturado em _input().
  4. Verificamos conflito com outras ações.
  5. Gravamos o novo evento no InputMap e atualizamos o texto do botão.
  6. 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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

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. A aba de vídeo tem a própria complexidade; para montá-la direito, com modos de janela e lista de resoluções, veja o guia de configurações de vídeo, resolução e tela cheia na Godot.

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.