Voltar para o Blog
Quest Log

Remapear Controles na Godot 4: Rebind em Tempo Real 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.

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.

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

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.