Voltar para o Blog
Quest Log

Portas Trancadas e Chaves no Godot 4

Personagem 2D em frente a uma porta trancada segurando uma chave vermelha brilhante.

Monte um sistema de porta e chave Godot 4 do zero: chave coletável, autoload de estado, abertura por proximidade e tecla, com código GDScript real.

Ferramenta:Godot

Quase todo jogo de exploração tem aquele momento: você acha uma porta fechada, dá meia volta no mapa, encontra a chave e volta para abrir. É uma mecânica simples de descrever e fácil de bagunçar na hora de programar. Neste tutorial você vai montar um sistema de porta e chave Godot 4 completo, do item coletável até a porta que verifica o que o player carrega e se abre com uma animação. Vamos cobrir os dois casos clássicos: chave genérica (um contador de chaves que abre qualquer porta) e chave com id específico (a chave vermelha só abre a porta vermelha).

A ideia é deixar o código limpo e reaproveitável. Em vez de cada porta guardar uma referência ao player, vamos centralizar o estado num autoload e usar grupos para identificar quem é o player. Assim você adiciona portas e chaves novas só arrastando cenas, sem mexer em lógica.

Portas Trancadas e Chaves no Godot 4

A estrutura do sistema de porta e chave Godot 4 tem três peças: um autoload que guarda quais chaves o player tem, a cena da chave que coleta esse item ao encostar, e a cena da porta que consulta o autoload quando o player tenta interagir. Cada peça é pequena e não conhece os detalhes das outras. A chave só sabe registrar um id. A porta só sabe perguntar se aquele id existe. O autoload é o ponto de encontro.

Antes de começar, marque seu player com um grupo. No editor, selecione o nó raiz do player, vá na aba Node, depois Groups, e adicione o grupo player. Tudo que precisar reconhecer o jogador vai checar esse grupo em vez de comparar nomes de nó. Se você quiser se aprofundar nesse padrão, eu escrevi um guia dedicado sobre grupos de nodes no Godot.

O autoload de inventário de chaves

O autoload é um singleton que existe durante toda a partida. Ele guarda um contador de chaves genéricas e um conjunto de ids de chaves específicas. Crie um script chaves_state.gd:

extends Node

# Chaves genéricas: qualquer porta comum aceita uma destas.
var chaves_genericas: int = 0

# Chaves com id: cada uma abre uma porta específica.
# Usamos um Dictionary como Set (o valor true é só marcador).
var chaves_ids: Dictionary = {}

signal chaves_mudaram

func adicionar_chave_generica(quantidade: int = 1) -> void:
    chaves_genericas += quantidade
    chaves_mudaram.emit()

func adicionar_chave_id(id: String) -> void:
    chaves_ids[id] = true
    chaves_mudaram.emit()

func tem_chave_id(id: String) -> bool:
    return chaves_ids.has(id)

func tem_chave_generica() -> bool:
    return chaves_genericas > 0

# Remove e retorna true se conseguiu gastar uma chave genérica.
func gastar_chave_generica() -> bool:
    if chaves_genericas <= 0:
        return false
    chaves_genericas -= 1
    chaves_mudaram.emit()
    return true

func gastar_chave_id(id: String) -> bool:
    if not chaves_ids.has(id):
        return false
    chaves_ids.erase(id)
    chaves_mudaram.emit()
    return true

Registre esse script como autoload em Project, Project Settings, Globals (Autoload). Dê o nome ChavesState. Agora qualquer script do projeto pode chamar ChavesState.tem_chave_id("chave_vermelha") sem precisar de referência.

O sinal chaves_mudaram é útil para atualizar a interface. Sempre que uma chave entra ou sai, a UI pode ouvir esse sinal e redesenhar o contador no canto da tela. Não vamos montar a HUD aqui, mas deixei o gancho pronto.

A chave como item coletável

A chave é uma Area2D que some quando o player encosta. Monte a cena assim:

  • Area2D (raiz) com o script abaixo
  • CollisionShape2D filho, com um círculo ou retângulo
  • Sprite2D filho com a arte da chave

No script, exporte duas propriedades: se a chave é genérica ou tem id, e qual é o id. Assim você reusa a mesma cena para todas as chaves do jogo, só mudando os campos no Inspector.

extends Area2D

# Se for false, a chave é genérica (incrementa o contador).
@export var tem_id: bool = false

# Id usado quando tem_id é true. Ex: "chave_vermelha".
@export var id_chave: String = ""

func _ready() -> void:
    body_entered.connect(_ao_entrar_corpo)

func _ao_entrar_corpo(corpo: Node2D) -> void:
    if not corpo.is_in_group("player"):
        return

    if tem_id:
        ChavesState.adicionar_chave_id(id_chave)
    else:
        ChavesState.adicionar_chave_generica()

    # Remove a chave do mundo depois de coletar.
    queue_free()

Repare que a checagem is_in_group("player") evita que inimigos ou projéteis coletem a chave por engano. Para a Area2D detectar o player, confirme que o player é um CharacterBody2D (ou RigidBody2D) e que as camadas de física batem: a Area2D precisa ter o player na sua Mask. Isso se configura no Inspector da Area2D, em Collision, Mask.

Se você já tem um sistema de inventário de itens no jogo, dá para tratar a chave como mais um item em vez de um contador isolado. A lógica de coletar é a mesma, muda só onde você guarda o estado. Vale a pena olhar como estruturar isso no post sobre criar um sistema de inventário para RPG.

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

A porta que verifica a chave

A porta é um StaticBody2D com colisão, porque ela bloqueia o movimento do player enquanto estiver trancada. Para detectar que o player chegou perto e quer interagir, adicionamos uma Area2D filha, maior que a porta, funcionando como zona de interação.

Monte a cena da porta assim:

  • StaticBody2D (raiz) com o script
  • CollisionShape2D filho, a colisão sólida que bloqueia
  • Sprite2D ou AnimatedSprite2D filho com a arte
  • Area2D filho chamado ZonaInteracao, com seu próprio CollisionShape2D maior

Antes do código, crie a ação de input. Vá em Project Settings, Input Map, e adicione uma ação chamada interagir. Mapeie para a tecla E ou para o botão que preferir.

extends StaticBody2D

# Se true, a porta precisa de uma chave com id específico.
@export var precisa_id: bool = false

# Id exigido quando precisa_id é true.
@export var id_exigido: String = ""

# Se true, a chave é consumida ao abrir (uso único).
@export var consome_chave: bool = true

@onready var colisao: CollisionShape2D = $CollisionShape2D
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var zona: Area2D = $ZonaInteracao

var player_perto: bool = false
var aberta: bool = false

func _ready() -> void:
    zona.body_entered.connect(_ao_player_chegar)
    zona.body_exited.connect(_ao_player_sair)

func _ao_player_chegar(corpo: Node2D) -> void:
    if corpo.is_in_group("player"):
        player_perto = true

func _ao_player_sair(corpo: Node2D) -> void:
    if corpo.is_in_group("player"):
        player_perto = false

func _unhandled_input(evento: InputEvent) -> void:
    if aberta or not player_perto:
        return
    if evento.is_action_pressed("interagir"):
        _tentar_abrir()

Toda a inteligência mora em _tentar_abrir(). Ele decide qual tipo de chave a porta exige, pergunta ao autoload se o player tem, e só então abre. Se for chave de uso único, gasta a chave no mesmo passo.

func _tentar_abrir() -> void:
    if precisa_id:
        if not ChavesState.tem_chave_id(id_exigido):
            _feedback_trancada()
            return
        if consome_chave:
            ChavesState.gastar_chave_id(id_exigido)
    else:
        if not ChavesState.tem_chave_generica():
            _feedback_trancada()
            return
        if consome_chave:
            ChavesState.gastar_chave_generica()

    _abrir()

func _abrir() -> void:
    aberta = true
    # Desabilita a colisão sólida sem quebrar o passo da física.
    colisao.set_deferred("disabled", true)
    # Toca a animação de abertura, se existir.
    if sprite.sprite_frames.has_animation("abrir"):
        sprite.play("abrir")

func _feedback_trancada() -> void:
    # Aqui você tocaria um som de "trancado" ou um shake.
    print("A porta está trancada.")

Por que set_deferred na colisão

Esse detalhe parece pequeno mas evita um bug chato. Quando você desabilita uma CollisionShape2D no meio do processamento de física, o Godot 4 pode reclamar que você está alterando o estado do espaço de física enquanto ele está travado para o passo atual. O set_deferred("disabled", true) agenda a mudança para o fim do frame, quando é seguro mexer. Use sempre essa forma ao ligar ou desligar colisões em resposta a um evento de física ou input.

Se a sua porta é uma Area2D em vez de StaticBody2D, ou seja, ela não bloqueia o movimento e serve só como gatilho que leva o player para outra sala (como no tutorial de teleporte e portal 2D no Godot), o mesmo cuidado vale. Mas para uma porta que precisa segurar o player do lado de fora até abrir, StaticBody2D com colisão sólida é a escolha certa.

Chave genérica contra chave com id

Vale entender quando usar cada modo. A chave genérica é boa para masmorras com várias portas iguais, no estilo dos jogos antigos onde você junta chaves e elas valem para qualquer fechadura comum. O player administra um recurso: tem três chaves, escolhe quais portas abrir.

A chave com id resolve puzzles e progressão. A chave vermelha abre só a porta vermelha, ponto. Isso garante que o jogador siga uma ordem ou encontre uma área específica antes de avançar. Como guardamos os ids num Dictionary, checar e remover é rápido e o código não muda conforme você adiciona ids novos. Quer a chave do chefe, a chave da masmorra de gelo e a chave secreta? São só três strings diferentes.

Você também pode misturar os dois. Uma porta com precisa_id = false aceita qualquer chave genérica, enquanto a porta do final do mapa exige id_exigido = "chave_mestra" e nunca consome a chave, deixando consome_chave = false para o player poder reabrir depois.

Conectando chaves a um sistema de drop

Em muitos jogos a chave não fica parada num pedestal: ela cai de um inimigo ou de um baú destruído. Se for esse o seu caso, basta instanciar a cena da chave na posição do drop quando o inimigo morre. A lógica de coleta que escrevemos continua igual, porque a chave já reage sozinha ao body_entered. Para montar essa parte, o tutorial de loot e drop de itens no Godot mostra como soltar cenas no mundo de forma organizada.

Testando o fluxo

Para validar tudo, monte uma cena de teste com o player, uma chave genérica solta no chão e uma porta com precisa_id = false. Ande até a chave, confira no debug que ChavesState.chaves_genericas virou 1, vá até a porta, aperte a tecla de interagir e veja a colisão sumir. Depois troque a porta para precisa_id = true com um id que você não coletou e confirme que ela responde "A porta está trancada" e não abre.

Com essas três peças no lugar, você tem um sistema de porta e chave Godot 4 que escala sem dor. Adicionar conteúdo novo é só duplicar cenas e preencher campos no Inspector. O autoload mantém o estado limpo, os grupos mantêm a identificação simples, e o set_deferred mantém a física feliz. A partir daqui dá para enfeitar: som de chave girando, partícula ao abrir, ou uma trava que pede duas chaves ao mesmo tempo. A base já aguenta tudo isso.