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.

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.

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

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 de transição de cena, 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.