Voltar para o Blog
Quest Log

Suporte a Controle (Gamepad) no Godot: Detecção, Deadzone e Ícones Dinâmicos

Gamepad flutuando em um ambiente de desenvolvimento de jogos com anéis de luz saindo do analógico

Tutorial completo de gamepad no Godot 4: detecte controles conectados, configure deadzone, vibração e troque ícones de botão conforme o dispositivo.

Suporte a Controle (Gamepad) no Godot: Detecção, Deadzone e Ícones Dinâmicos

Suportar gamepad no Godot é mais fácil do que parece, e ao mesmo tempo é onde muito jogo indie escorrega. O Input Map resolve o básico em cinco minutos, mas aí o jogador desconecta o controle no meio da fase, o personagem anda sozinho por causa de drift no analógico, e o tutorial mostra "aperte E" pra quem está de controle na mão. São esses detalhes que separam um port de PC preguiçoso de um jogo que parece feito pra controle.

Esse tutorial cobre o caminho completo: mapear ações pra gamepad, detectar conexão e desconexão, configurar deadzone do jeito certo, trocar os ícones de botão conforme o dispositivo e usar vibração. Todo código é GDScript do Godot 4.x e roda como está.

Como o Godot enxerga o controle

Antes do código, vale entender o que a engine faz por você. O Godot usa o banco de mapeamentos da SDL, o mesmo que a Steam e meio mundo dos jogos usam. Na prática isso significa que um controle de Xbox, um DualSense e um Pro Controller chegam pro seu código com o mesmo layout lógico: o botão de baixo é sempre JOY_BUTTON_A, o analógico esquerdo é sempre JOY_AXIS_LEFT_X e JOY_AXIS_LEFT_Y, não importa a marca.

Cada controle conectado recebe um número de dispositivo, começando do 0. Num jogo single player você quase sempre trabalha com o dispositivo 0; em multiplayer local, cada jogador fica amarrado a um número.

Os eixos do analógico vão de -1.0 a 1.0 (esquerda/cima negativo, direita/baixo positivo), e os gatilhos (JOY_AXIS_TRIGGER_LEFT e JOY_AXIS_TRIGGER_RIGHT) vão de 0.0 a 1.0. Guarda isso, porque é a base de tudo que vem a seguir.

Mapeando ações no Input Map

A regra de ouro de input no Godot: nunca cheque tecla ou botão direto no código de gameplay. Você cria uma ação ("jump", "attack", "move_left") e pendura nela quantos dispositivos quiser. O código pergunta pela ação, e o jogador escolhe como apertar.

No editor: Project > Project Settings > Input Map. Crie a ação, clique no "+" ao lado dela e pressione o botão do controle (ou mova o analógico) que quer associar. Pode colocar teclado e gamepad na mesma ação, e é exatamente isso que você deve fazer.

Com as ações configuradas, o código de movimento fica idêntico pra qualquer dispositivo:

func _physics_process(delta):
    # Funciona com WASD, setas ou analógico, depende só do Input Map.
    var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = direction * SPEED
    move_and_slide()

    if Input.is_action_just_pressed("jump"):
        pular()

Se precisar adicionar um binding por código (útil pra menu de remapeamento de controles), o InputMap aceita eventos em runtime:

func _ready():
    var jump_button = InputEventJoypadButton.new()
    jump_button.button_index = JOY_BUTTON_A
    InputMap.action_add_event("jump", jump_button)

Detalhe que confunde todo mundo: os nomes JOY_BUTTON_A, JOY_BUTTON_B etc. seguem a posição física do layout Xbox. No DualSense, JOY_BUTTON_A é o X (cruz). O Godot abstrai a posição, não o desenho do botão. É por isso que ícone dinâmico importa, e a gente chega lá.

Detectando o gamepad no Godot: conexão e desconexão

O jogador liga o controle depois do jogo aberto, a pilha acaba no meio da boss fight, o cabo solta. Seu jogo precisa reagir a tudo isso, e o Godot entrega de bandeja com duas ferramentas: Input.get_connected_joypads() pra saber o estado atual, e o sinal Input.joy_connection_changed pra reagir a mudanças.

extends Node

func _ready():
    # Estado inicial: o que já está plugado quando o jogo abre.
    for device in Input.get_connected_joypads():
        print("Controle %d: %s" % [device, Input.get_joy_name(device)])

    # Reage a conectar/desconectar daqui pra frente.
    Input.joy_connection_changed.connect(_on_joy_connection_changed)

func _on_joy_connection_changed(device: int, connected: bool):
    if connected:
        print("Conectado: ", Input.get_joy_name(device))
    else:
        print("Controle %d caiu" % device)
        _pausar_por_desconexao()

func _pausar_por_desconexao():
    get_tree().paused = true
    # Aqui você mostra um popup "Controle desconectado" e espera reconectar.

Pausar quando o controle cai não é frescura: é requisito de certificação em console e cortesia básica no PC. Ninguém merece morrer pro chefe porque o Bluetooth engasgou.

O Input.get_joy_name(device) retorna o nome legível do controle ("Xbox Series Controller", "PS5 Controller" e por aí vai). Esse nome vai ser a chave dos ícones dinâmicos mais pra frente.

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

Deadzone: o ajuste que ninguém vê e todo mundo sente

Analógico em repouso nunca marca exatamente zero. Sensor barato, mola gasta, poeira: sempre tem um ruído tipo 0.03 num eixo. Sem deadzone, esse ruído vira personagem andando sozinho, câmera derivando devagar, drift. A deadzone é a faixa em volta do centro que o jogo trata como zero.

No Godot 4, cada ação do Input Map tem a própria deadzone, configurável ali no editor ao lado da ação. E o Input.get_vector() aplica essa deadzone de forma radial: ele mede o comprimento do vetor completo, não cada eixo separado. Isso importa porque deadzone por eixo (axial) cria um efeito de "cruz" no centro do analógico, onde movimentos diagonais leves grudam nos eixos. A radial mantém o movimento circular suave.

Tradução prática: se você usa Input.get_vector() pra movimento, a deadzone certa já está aplicada e você não precisa fazer nada. É mais um motivo pra preferir ele em vez de ler eixo cru.

Agora, quando você precisa ler o eixo cru (uma câmera com curva de resposta própria, por exemplo), a deadzone vira sua responsabilidade. O jeito certo é radial e com remapeamento:

func get_stick(device: int) -> Vector2:
    var raw = Vector2(
        Input.get_joy_axis(device, JOY_AXIS_LEFT_X),
        Input.get_joy_axis(device, JOY_AXIS_LEFT_Y)
    )

    var deadzone = 0.2
    if raw.length() < deadzone:
        return Vector2.ZERO

    # Remapeia: logo após a deadzone começa em 0, no máximo chega em 1.
    # Sem isso, o input "salta" de 0 pra 0.2 e o movimento fino some.
    return raw.normalized() * ((raw.length() - deadzone) / (1.0 - deadzone))

O remapeamento da última linha é o detalhe que quase todo tutorial pula. Se você só zera abaixo da deadzone e devolve o valor cru acima dela, o jogador perde a faixa mais delicada do analógico: o input pula de parado pra 20% de velocidade sem nada no meio. Com o remap, a resposta volta a ser contínua de 0 a 1.

Sobre o valor: 0.2 é um ponto de partida razoável pra movimento. Deadzone muito baixa deixa drift passar em controle usado; muito alta come a precisão de quem tem controle bom. Se o seu jogo pede mira fina, vale expor o valor num slider de opções e deixar o jogador decidir.

Ícones de botão dinâmicos

Mostrar "pressione A" pra quem está no DualSense é o jeito mais rápido de parecer amador. A solução tem duas partes: descobrir qual dispositivo o jogador está usando agora, e trocar as texturas da UI quando isso muda.

A detecção se faz observando o último evento de input que chegou. Esse script funciona bem como autoload (Project Settings > Globals), porque o jogo inteiro vai querer consultar:

# input_device_manager.gd (autoload)
extends Node

signal device_changed(device_type: DeviceType)

enum DeviceType { KEYBOARD, XBOX, PLAYSTATION, NINTENDO, GENERIC_GAMEPAD }

var current_device := DeviceType.KEYBOARD

func _input(event):
    var detected := current_device

    if event is InputEventKey or event is InputEventMouseButton:
        detected = DeviceType.KEYBOARD
    elif event is InputEventJoypadButton:
        detected = _identify_gamepad(event.device)
    elif event is InputEventJoypadMotion and abs(event.axis_value) > 0.5:
        # O limiar evita que ruído de drift "roube" a UI pro gamepad.
        detected = _identify_gamepad(event.device)

    if detected != current_device:
        current_device = detected
        device_changed.emit(current_device)

func _identify_gamepad(device: int) -> DeviceType:
    var joy_name = Input.get_joy_name(device).to_lower()
    if joy_name.contains("xbox"):
        return DeviceType.XBOX
    if joy_name.contains("ps5") or joy_name.contains("ps4") \
            or joy_name.contains("dualsense") or joy_name.contains("dualshock"):
        return DeviceType.PLAYSTATION
    if joy_name.contains("nintendo") or joy_name.contains("pro controller") \
            or joy_name.contains("joy-con"):
        return DeviceType.NINTENDO
    return DeviceType.GENERIC_GAMEPAD

Dois pontos desse código que valem atenção. Primeiro, o limiar de 0.5 no InputEventJoypadMotion: sem ele, qualquer ruído de analógico dispara troca de ícone, e a UI fica piscando entre teclado e controle. Segundo, a identificação por nome é heurística mesmo; controle genérico ou de marca menor cai no GENERIC_GAMEPAD, e pra esse caso o layout Xbox é a convenção segura.

Do lado da UI, cada elemento que mostra botão escuta o sinal e troca a textura:

extends TextureRect

@export var keyboard_icon: Texture2D
@export var xbox_icon: Texture2D
@export var playstation_icon: Texture2D
@export var nintendo_icon: Texture2D

func _ready():
    InputDeviceManager.device_changed.connect(_on_device_changed)
    _on_device_changed(InputDeviceManager.current_device)

func _on_device_changed(device_type):
    match device_type:
        InputDeviceManager.DeviceType.KEYBOARD:
            texture = keyboard_icon
        InputDeviceManager.DeviceType.XBOX, InputDeviceManager.DeviceType.GENERIC_GAMEPAD:
            texture = xbox_icon
        InputDeviceManager.DeviceType.PLAYSTATION:
            texture = playstation_icon
        InputDeviceManager.DeviceType.NINTENDO:
            texture = nintendo_icon

Pras texturas em si, não precisa desenhar nada: o pacote Kenney Input Prompts é gratuito, domínio público (CC0) e cobre Xbox, PlayStation, Nintendo, teclado e mouse no mesmo estilo. É o que eu uso e o que recomendo pra qualquer projeto que não tenha artista de UI dedicado.

Vibração

Vibração é feedback barato e subutilizado. O Godot expõe os dois motores do controle direto no singleton Input:

# device, motor fraco (0-1), motor forte (0-1), duração em segundos
Input.start_joy_vibration(0, 0.3, 0.8, 0.4)

# Parar antes da hora (duração 0 = vibra até você mandar parar):
Input.stop_joy_vibration(0)

O motor "fraco" é o de frequência alta (aquele zumbido fino) e o "forte" é o de frequência baixa (o tranco surdo). Na prática: impacto pesado pede motor forte curto, algo elétrico ou contínuo pede o fraco. Um hit de espada com (0.0, 0.7, 0.15) já muda a sensação do combate.

Só não exagera. Vibração constante cansa e mascara os momentos que importam. E ofereça a opção de desligar nas configurações; tem jogador que odeia, e tem controle no qual a vibração drena a bateria visivelmente.

Testando sem sofrer

Três hábitos que economizam tempo nessa área:

  • Teste com mais de um controle físico. O mapeamento SDL resolve muito, mas controle genérico de marca duvidosa apronta. Se o jogo vai pra Steam, um Xbox e um DualSense cobrem a maioria dos jogadores.
  • Teste desconexão de verdade. Tira a pilha, desliga o Bluetooth, puxa o cabo no meio do gameplay. O sinal joy_connection_changed só protege o que você tratou.
  • Deixe o teclado sempre funcional. Mesmo num jogo "feito pra controle", você vai iterar mil vezes no editor, e alt-tab com gamepad é miserável.

Fechando

Suporte a gamepad no Godot se resume a quatro decisões bem feitas: ações no Input Map em vez de botão hardcoded, reação a conexão e desconexão via joy_connection_changed, deadzone radial com remapeamento quando você lê eixo cru, e um autoload simples que detecta o dispositivo ativo pra UI mostrar o ícone certo.

Nada disso é difícil. O que diferencia é fazer tudo, porque o jogador não percebe input bem feito, ele só percebe quando falta. Monta o gerenciador de dispositivo num projeto de teste, pluga dois controles diferentes e observa a UI trocar sozinha. Depois disso, levar pro seu jogo é copiar um autoload e três scripts.