Voltar para o Blog
Quest Log

Teleporte e portais em 2D no Godot 4

Dois portais brilhantes conectados em uma fase de plataforma 2D com o player atravessando.

Aprenda a criar portal godot 2d com Area2D, teleporte sem loop infinito, par de portais ligados, preservacao de velocidade e efeito visual na transicao.

Ferramenta:Godot

Portais sao uma das mecanicas mais satisfatorias de implementar em um jogo de plataforma ou puzzle. O jogador entra em um lugar e sai em outro, e quando funciona bem parece magica. Montar um portal godot 2d e simples na ideia: uma area que detecta o player e o move para um ponto de destino. O problema aparece nos detalhes. Se voce so reposicionar o player, ele cai dentro do portal de destino, que dispara o teleporte de volta, e voce entra num loop infinito que trava o jogo ou faz o personagem piscar entre os dois pontos.

Neste tutorial a gente resolve isso de verdade. Vamos montar um portal como Area2D, ligar dois portais em par, tratar o loop de re-entrada com um cooldown, preservar a velocidade do player na saida e adicionar um efeito visual rapido na transicao. No fim eu mostro a diferenca entre teleportar dentro da mesma cena e trocar de cena inteira, que sao problemas diferentes com solucoes diferentes.

A estrutura basica de um portal godot 2d e um Area2D com um CollisionShape2D para a zona de deteccao e uma referencia para onde o player deve aparecer. O destino pode ser outro portal ou um simples Node2D marcador na cena. Vamos usar o @export para escolher o destino direto no editor, sem precisar fixar caminho no codigo.

A cena do portal

Crie uma cena com esta arvore:

  • Area2D (raiz, script portal.gd)
  • CollisionShape2D
  • Sprite2D ou AnimatedSprite2D para o visual
  • Marker2D chamado SpawnPoint (onde o player vai surgir)

O Marker2D separado e importante. Voce nao quer que o player apareca no centro exato do portal de destino, porque ai ele vai estar dentro da zona de deteccao. Coloque o SpawnPoint um pouco a frente da boca do portal, na direcao em que o jogador deve sair.

O script do portal

extends Area2D
class_name Portal

# Outro portal para onde este leva. Configurado no editor.
@export var destino: Portal

# Tempo que o portal de destino fica "surdo" depois de receber alguem.
@export var cooldown: float = 0.4

# Marcador interno de onde o player aparece ao chegar aqui.
@onready var spawn_point: Marker2D = $SpawnPoint

# Quando true, este portal ignora qualquer corpo que entrar.
var bloqueado: bool = false


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


func _on_body_entered(body: Node2D) -> void:
    if bloqueado:
        return
    if destino == null:
        push_warning("Portal sem destino configurado: %s" % name)
        return
    if not body.is_in_group("player"):
        return
    _teleportar(body)


func _teleportar(body: Node2D) -> void:
    # Bloqueia o portal de DESTINO para ele nao mandar o player de volta.
    destino.bloquear_temporariamente()
    # Move o player para o ponto de saida do portal de destino.
    body.global_position = destino.spawn_point.global_position


func bloquear_temporariamente() -> void:
    bloqueado = true
    var timer := get_tree().create_timer(cooldown)
    timer.timeout.connect(func() -> void: bloqueado = false)

O ponto central aqui e o bloquear_temporariamente(). Quando o portal A teleporta o player, ele nao bloqueia a si mesmo. Ele bloqueia o portal B, que e onde o player vai cair. Assim que o player entra na area do B, o B esta surdo e ignora a entrada. Depois do cooldown, o B volta a funcionar normalmente. Se o jogador quiser voltar pelo B, ele precisa sair e entrar de novo, o que e exatamente o comportamento esperado.

Repare que eu uso o grupo player para filtrar quem dispara o portal. Adicione o no do player ao grupo player no editor ou via codigo com add_to_group("player") no _ready() dele. Sem esse filtro, qualquer inimigo ou caixa fisica passaria pelo portal tambem, o que as vezes voce quer, mas e melhor decidir isso de proposito.

Ligando os portais em par

Com o @export var destino voce abre a cena da fase, seleciona o Portal A e arrasta o Portal B para o campo destino no inspetor. Depois faz o inverso no Portal B, apontando para o A. Pronto, A leva a B e B leva a A.

Se voce tem varios pares na mesma fase, cada par fica independente porque o bloqueio e por instancia, nao global. Um jogador atravessando o par 1 nao interfere no par 2.

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

Preservando velocidade e direcao

Teleportar so a posicao funciona, mas o resultado fica seco. Se o player entrou correndo para a direita e sai parado, quebra a sensacao de continuidade. O ideal e levar a velocidade junto. Como o player normalmente tem seu proprio velocity (em um CharacterBody2D), basta nao zerar nada e o movimento continua. Mas se voce quer rotacionar a direcao, por exemplo um portal na parede que cospe o player para cima, da para reorientar o vetor de velocidade usando o angulo entre os dois portais.

func _teleportar(body: Node2D) -> void:
    destino.bloquear_temporariamente()
    body.global_position = destino.spawn_point.global_position

    # Se o corpo for um CharacterBody2D, reorienta a velocidade
    # para a direcao em que o portal de destino aponta.
    if body is CharacterBody2D:
        var corpo := body as CharacterBody2D
        var rotacao := destino.spawn_point.global_rotation - global_rotation
        corpo.velocity = corpo.velocity.rotated(rotacao)

Aqui eu pego a diferenca de rotacao entre o ponto de entrada e o ponto de saida e aplico essa rotacao no vetor de velocidade. Se os dois portais apontam para o mesmo lado, a rotacao e zero e nada muda. Se o portal de destino esta girado noventa graus, a velocidade gira junto e o player sai na direcao certa, mantendo a inercia. Para manter simples, deixe os SpawnPoint com a rotacao apontando para a saida desejada.

Efeito visual na transicao

Um portal sem feedback visual confunde o jogador. Ele nao sabe se foi teleportado ou se algo bugou. Um flash rapido ou um burst de particulas resolve isso. A forma mais barata e um fade curto na tela inteira, mas para portais dentro da mesma cena costuma ser melhor um efeito local, no proprio personagem, para nao cobrir a acao.

Vou usar um GPUParticles2D de one shot no ponto de entrada e no de saida. Adicione um GPUParticles2D chamado Burst na cena do portal, marque one_shot como true e emitting como false.

@onready var burst: GPUParticles2D = $Burst


func _teleportar(body: Node2D) -> void:
    destino.bloquear_temporariamente()
    _emitir_efeito()           # efeito na entrada
    body.global_position = destino.spawn_point.global_position
    destino._emitir_efeito()   # efeito na saida

    if body is CharacterBody2D:
        var corpo := body as CharacterBody2D
        var rotacao := destino.spawn_point.global_rotation - global_rotation
        corpo.velocity = corpo.velocity.rotated(rotacao)


func _emitir_efeito() -> void:
    burst.restart()
    burst.emitting = true

O restart() garante que o sistema de particulas reinicie do zero mesmo se ainda estiver no meio de uma emissao anterior. Isso evita aquele bug em que o efeito so dispara na primeira vez. Se voce preferir um flash de cor, da para colocar um ColorRect branco com modulate.a indo de 1 a 0 via Tween rapido, algo como meio segundo, mas particulas tendem a parecer mais polidas para portais.

Se voce quiser um fade de tela cheia bem feito para momentos de impacto, vale ver como montar uma transicao de cena com fade reaproveitavel, que serve tanto para portais grandes quanto para mudanca de fase.

Teleporte na mesma cena versus troca de cena

Os portais ate aqui movem o player dentro da mesma cena. Isso e instantaneo e simples porque o no do player continua existindo, voce so muda a posicao dele. A camera segue, a fisica continua, nada e recarregado.

Trocar de cena e outra historia. Quando o portal leva para outra fase, o player precisa ser destruido com a cena atual e recriado na nova, ou voce mantem informacao de estado para reconstruir onde ele deve aparecer. O fluxo muda bastante.

@export var cena_destino: PackedScene
@export var id_spawn: String = "default"


func _on_body_entered(body: Node2D) -> void:
    if bloqueado or not body.is_in_group("player"):
        return

    if cena_destino != null:
        _trocar_de_cena()
    elif destino != null:
        _teleportar(body)


func _trocar_de_cena() -> void:
    # Guarda em algum autoload onde o player deve aparecer na proxima cena.
    GameState.proximo_spawn = id_spawn
    get_tree().change_scene_to_packed(cena_destino)

A diferenca pratica e que na troca de cena voce nao tem mais o player do outro lado, entao nao da para arrastar a velocidade dele direto. Voce precisa de um autoload, aqui chamei de GameState, para guardar onde o player deve aparecer. Na nova cena, um script de inicializacao le GameState.proximo_spawn e posiciona o player no marcador correto. Por isso eu passo um id_spawn, um texto que identifica qual ponto de entrada usar na proxima fase.

Para teleporte na mesma cena, prefira sempre mover a posicao. E mais barato, mais suave e mais facil de depurar. Reserve a troca de cena para quando voce realmente muda de mapa.

Cuidados comuns

Alguns problemas aparecem com frequencia quando os portais comecam a ser testados de verdade.

O primeiro e o tamanho do CollisionShape2D de deteccao. Se for pequeno demais, o player atravessa em alta velocidade sem disparar o portal, porque a fisica deu um passo grande entre dois quadros. Deixe a zona com uma largura razoavel ou ative o Continuous CD no player se ele for muito rapido.

O segundo e esquecer de colocar o player no grupo player. Sem o grupo, o portal simplesmente nao reage e parece que o codigo esta errado quando na verdade o filtro esta correto.

O terceiro e posicionar o SpawnPoint dentro da area do portal de destino. Mesmo com o cooldown isso causa comportamento estranho se o cooldown for curto demais. Coloque o SpawnPoint claramente fora da zona de deteccao, na boca do portal apontando para fora.

Se o seu portal precisa de uma condicao para abrir, como o jogador ter pego um item, a logica e parecida com a de portas trancadas e chaves, so que em vez de tocar para abrir, o portal verifica a condicao antes de teleportar.

Portal de mao unica e com condicao de entrada

Nem todo portal e um par simetrico. As vezes voce quer um portal que so funciona em um sentido, por exemplo um cano que te leva para baixo mas nao de volta, ou um teleporte que so abre depois que o jogador apertou um botao. A base e a mesma, voce so adiciona uma guarda antes de teleportar.

# No topo do portal.gd, junto dos outros @export.
@export var mao_unica: bool = false
@export var ativo: bool = true


func _on_body_entered(body: Node2D) -> void:
    if bloqueado or not ativo:
        return
    if destino == null or not body.is_in_group("player"):
        return
    _teleportar(body)
    # Portal de mao unica se desliga depois de usar uma vez.
    if mao_unica:
        ativo = false

Com ativo exposto no editor, um botao ou um gatilho pode religar o portal com portal.ativo = true via sinal. A logica de liberar o portal por uma condicao, como ter pego uma chave, e a mesma de portas trancadas e chaves no Godot: em vez de abrir uma porta, voce vira a flag ativo do portal quando o item entra no inventario.

Por que a deteccao as vezes falha

Quando o portal "nao pega" o player, o problema quase nunca esta no teleporte, esta na deteccao. Portal e um Area2D, e vale entender como ele enxerga os corpos antes de culpar o codigo. A diferenca entre detectar com sinal de corpo e detectar com sobreposicao de areas muda o comportamento, e eu destrinchei isso em Area2D versus corpos de fisica no Godot. Dois ajustes resolvem a maioria dos casos:

  • Collision layer e mask. O Area2D do portal precisa ter na sua mask a mesma layer em que o player esta. Se o player esta na layer 1, a mask do portal tem que incluir a layer 1, senao o body_entered nunca dispara, mesmo com o CollisionShape2D perfeito.
  • Player rapido demais. Em alta velocidade a fisica pode "pular" por cima da area fina entre dois frames. Alem de alargar o shape, ative o Continuous CD no CharacterBody2D do player.

Reposicionando a camera no teleporte

Um detalhe que passa batido: se a sua camera suaviza o movimento (smoothing ligado), teleportar o player faz a camera deslizar atravessando a fase inteira ate alcancar o destino, o que fica horrivel. O conserto e resetar a posicao da camera no mesmo frame do teleporte.

func _teleportar(body: Node2D) -> void:
    destino.bloquear_temporariamente()
    body.global_position = destino.spawn_point.global_position

    # Zera o deslize da camera para ela nao atravessar a fase.
    var cam := body.get_node_or_null("Camera2D") as Camera2D
    if cam != null:
        cam.reset_smoothing()

O metodo reset_smoothing() força a camera a ir direto para a posicao de destino, sem interpolar. Se voce usa uma camera que segue o jogador com atraso, vale conferir como ela e montada em camera 2D que segue o jogador no Godot, porque o ponto de ancoragem influencia onde o player aparece visualmente depois do salto. A mesma ideia de "fixar o estado no instante da transicao" aparece quando o portal leva para um ponto de retorno: nesse caso a logica conversa direto com checkpoint e respawn no Godot, que guarda onde o jogador deve reaparecer.

Teleportando caixas e inimigos sem brigar com a fisica

Cedo ou tarde voce vai querer que o portal aceite mais do que o player: uma caixa que o jogador empurra, um inimigo em perseguicao, um projetil. Dois detalhes mudam em relacao ao CharacterBody2D, e os dois causam bug dificil de rastrear se voce nao souber que existem.

O primeiro e o momento do teleporte. O sinal body_entered dispara durante o passo de fisica, e mexer no estado fisico de um corpo nesse instante pode gerar o erro classico "Can't change this state while flushing queries. Use call_deferred() or set_deferred()". Com CharacterBody2D mover a posicao costuma passar sem reclamacao, mas com RigidBody2D o aviso aparece rapido no console. A solucao e adiar a chamada para depois que a fisica terminou o frame:

func _on_body_entered(body: Node2D) -> void:
    if bloqueado or destino == null:
        return
    if body.is_in_group("player") or body.is_in_group("teleportavel"):
        _teleportar.call_deferred(body)

O call_deferred garante que o _teleportar so rode quando for seguro mudar posicao e velocidade. Custa um frame de atraso que ninguem percebe, e vale adotar como padrao em qualquer logica disparada por body_entered que mova corpos.

O segundo detalhe e a velocidade. O CharacterBody2D guarda o movimento em velocity, que e so uma variavel sua, mas o RigidBody2D usa linear_velocity e angular_velocity, controladas pelo motor de fisica. Para o corpo rigido sair do outro lado com a mesma inercia, rotacione a linear_velocity do mesmo jeito que fizemos com o player:

func _teleportar_rigido(corpo: RigidBody2D) -> void:
    destino.bloquear_temporariamente()
    var rotacao := destino.spawn_point.global_rotation - global_rotation
    corpo.global_position = destino.spawn_point.global_position
    corpo.linear_velocity = corpo.linear_velocity.rotated(rotacao)

Se depois do salto a caixa tremer ou afundar no chao, congele o corpo durante o reposicionamento: marque corpo.freeze = true antes de mover e devolva freeze para false com set_deferred logo em seguida. Isso tira o corpo do controle do motor de fisica pelo instante exato da mudanca e elimina o conflito entre a posicao que voce quer e a que o motor estava integrando. Se a diferenca entre esses tipos de corpo ainda confunde, o guia de fisica de jogos no Godot destrincha quando usar cada um.

Um ultimo cuidado com projeteis: o cooldown continua obrigatorio. Um tiro rapido entrando no portal A e saindo no B que aponta de volta para o A cria um pingue-pongue infinito se os dois estiverem destravados ao mesmo tempo. O mesmo bloquear_temporariamente() do player resolve, so confira que o projetil esta num grupo aceito pelo filtro.

Fechando

Com Area2D, um Marker2D de saida e a flag de cooldown no portal de destino, voce tem portais robustos sem loop infinito. Adicione a reorientacao de velocidade e um burst de particulas e a mecanica passa de funcional para gostosa de usar. Comece pelo teleporte na mesma cena, que cobre a maioria dos casos, e so parta para troca de cena quando o destino for outra fase. A partir dessa base voce monta portais coloridos estilo puzzle, teleportes de checkpoint e ate atalhos secretos, sempre com o mesmo nucleo de codigo.