Gerenciador de Mensagens Global

Dicas Godot

Uma maneira simples de implementar o padrão Observer

Por: George Marques Publicado em: 23 de junho de 2025

Foto de um papel dentro de uma garrafa na areia da praia

Sinais são um recurso poderoso do Godot. Eles permitem que mensagens sejam transmitidas sem que o emissor saiba quem realmente se inscreveu para recebê-las. No entanto, às vezes, o assinante também não sabe sobre o emissor. Manter uma referência ao objeto pode ser complicado e pode acoplar seu projeto de maneiras que você não deseja. É aí que um gerenciador de sinais global pode vir a calhar.

Padrão Observer

A palavra “global” às vezes soa como um monstro assustador no mundo da programação. Embora seja verdade que coisas globais possam se virar contra você de vez em quando, elas ainda são uma boa escolha para muitas coisas. Uma delas é o chamado padrão observer.

O objetivo principal deste padrão é desacoplar o código que sabe sobre um evento do código que precisa tomar uma ação (não relacionada) quando isso acontece. Isso permite que os objetos se comuniquem sem necessariamente saberem uns dos outros. Acho que não consigo explicar isso melhor do que Bob Nystrom, então, para uma explicação mais aprofundada do padrão, clique no link anterior (o texto está em inglês, um tradutor automático é útil caso o idioma seja um problema).

Um projeto de exemplo

Para demonstrar uma implementação do gerenciador de sinais, vamos construir um projeto bem simples. Faremos nosso clássico icon.pngsvg se mover para a esquerda ou direita de acordo com a tecla pressionado. Quando ele cair da plataforma, ele reaparecerá em um ponto específico.

Abra o Godot e crie um novo projeto em algum lugar. Vamos começar com o _ para resolver o problema. Vamos criar duas ações: move_left e move_right. Estou atribuindo as teclas A e D a elas, pois são comuns e facilmente acessíveis em um teclado comum. Por consistência com o projeto de demonstração, vamos manter os nomes em inglês.

Configurações do mapa de entrada adicionando as ações 'move_left' e 'move_right'

Os passos aqui pressupõem que você já esteja familiarizado com o Godot. Se precisar de uma referência, você pode encontrar o projeto final em nosso GitHub. Ele também não segue todas as práticas recomendadas para que possamos ter algo que se encaixe em um artigo.

Configurando a cena de exemplo

Vamos criar uma nova cena com um Node2D como raiz. Vou chamá-la de Main, pois é meu nome padrão para o nó raiz. Abaixo dela, crie um StaticBody2D para servir como plataforma. Para configurá-la, adicione um CollisionShape2D e defina um RectangleShape2D com um tamanho razoável, mas que não precise ultrapassar o viewport. Adicione um Sprite2D também para que possamos vê-lo e use o icon.svg, sem esquecer de esticá-lo para corresponder à forma da colisão.

Com o piso no lugar, vamos criar nosso personagem. Adicione um apropriadamente chamado CharacterBody2D ao nó raiz. Adicione Sprite2D como seu filho e defina o icon.svg como a textura. Assim como no piso, crie um CollisionShape2D e adicione um RectangleShape2D a ele. Faça com que a forma corresponda ao ícone, o que pode ser feito facilmente definindo ambas as dimensões para 128px. Adicione também uma Camera2D ao nó do personagem para que a visualização o acompanhe. Mova o nó do personagem para um local acima do piso.

Em seguida, vamos criar uma área grande para detectar quando o personagem cai. Primeiro, crie um Area2D sob o nó raiz. Como já estamos acostumados, adicione um CollisionShape2D e configure um RectangleShape2D. Coloque a área bem abaixo do piso e torne-a grande o suficiente para sempre capturar o personagem.

Para configurar nosso ponto de spawn visualmente, adicione um nó Marker2D sob a raiz e posicione-o próximo ao personagem (não precisa ser exato). O importante é que o jogador reapareça sobre o piso.

Vamos nomear nossos nós para facilitar a referência. O nó do piso é chamado apenas de Floor e, portanto, o personagem também é chamado de Character. A área é chamada de Catcher porque captura o jogador. O Marker2D pode ser renomeado para SpawnPoint. Neste ponto, você deve ter uma cena semelhante a esta:

Imagem de exemplo com a cena construída conforme descrito no texto

Movimento do personagem

Adicione um novo script ao nosso nó Character. Vamos aproveitar a física do Godot aqui para tornar o script de movimento bem simples. O código é o seguinte:

# character.gd
extends CharacterBody2D

const SPEED := 250.0
const GRAVITY := 980.0

func _physics_process(_delta: float) -> void:
	var direction := Input.get_axis("move_left", "move_right")
	velocity.x = SPEED * direction
	velocity.y = GRAVITY
	move_and_slide()

Novamente, tomamos algumas liberdades aqui por uma questão de brevidade (como usar a gravidade como velocidade em vez de aceleração).

Se você reproduzir a cena neste momento, já poderá mover o personagem para a esquerda e para a direita. Se você ultrapassar as bordas do chão, o personagem cairá para sempre, pois não há nada para impedi-lo. Se você habilitar Formas de Colisão Visíveis no menu Depuração, poderá ver onde a área está.

Manipulador de mensagens

Antes de conectarmos as coisas, precisamos do nosso manipulador. Crie um novo script estendendo Node e chame-o de message_handler.gd. Para torná-lo global, vamos adicioná-lo como Autoload nas Configurações do Projeto. Aceite o nome padrão após selecionar o script (MessageHandler) e certifique-se de que a opção Variável Global esteja habilitada.

Estamos adicionando duas mensagens para este exemplo. Uma para quando um corpo é capturado pela área e outra para dizer ao personagem para reaparecer. Veja como fica:

# message_handler.gd
extends Node

# Necessário porque não estamos usando os sinais neste script.
@warning_ignore_start("unused_signal")

sinal body_caught(corpo: PhysicsBody2D)
sinal respawn(para: Vector2)

Como você pode ver, estamos aproveitando o sistema de sinais embutido do Godot. Poderíamos criar uma abstração sobre ele para assinar e emitir eventos, mas esse é um trabalho desnecessário. Os sinais funcionam muito bem para isso e não precisamos substituí-los por outra coisa para justificar uma abstração.

Usando o gerenciador

Agora podemos criar um script no nó Main (a raiz) para solicitar um respawn sempre que o personagem for capturado.

# main.gd
extends Node2D

func _ready() -> void:
	MessageHandler.body_caught.connect(_on_body_caught)

func _on_body_caught(_body: PhysicsBody2D) -> void:
	MessageHandler.respawn.emit(($SpawnPoint as Marker2D).position)

Então, simplesmente conectamos um sinal e emitimos outro. A emissão é importante aqui porque o nó Main é quem conhece o SpawnPoint, então ele envia a posição dele.

O nó de área é responsável por emitir o outro sinal. Podemos fazer isso simplesmente com o seguinte script:

# catcher.gd
extends Area2D

func _ready() -> void:
	ody_entered.connect(_on_body_enter)

func _on_body_enter(body: PhysicsBody2D):
	MessageHandler.body_caught.emit(body)

Isso basicamente reemitirá seu próprio sinal body_entered com o sinal global. O interessante é que os observadores não precisarão encontrar o nó Catcher para se conectar a ele, já que podem usar o gerenciador global. Se você tiver uma conquista para quando o jogador cair 100 vezes, poderá colocar o contador em outro lugar e simplesmente se conectar ao sinal global.

Agora, o personagem precisa reagir ao sinal respawn e mudar sua própria posição. Podemos fazer isso adicionando duas funções ao script do personagem:

# character.gd
extends CharacterBody2D

...

func _ready() -> void:
	MessageHandler.respawn.connect(respawn)

func respawn(to: Vector2) -> void:
	position = to

Primeiramente, conectamos ao sinal e, sempre que ele é disparado, simplesmente redefinimos a posição para onde o sinal aponta. Se você tiver vários checkpoints e quiser que o jogador reapareça em pontos diferentes dependendo da distância percorrida, o script do personagem não precisa saber disso, pois ele apenas reage ao sinal.

Se você reproduzir a cena agora e mover o personagem até que ele caia, verá que ele está reaparecendo de volta para onde o marcador está.

Um uso extra

O movimento é muito abrupto durante o respawn, então vamos adicionar uma mensagem para informar o jogador que isso aconteceu.

Começaremos adicionando uma CanvasLayer sob o nó raiz para termos uma camada limpa para a UI . Em seguida, adicione uma Label, chame-a de Respawned e defina o texto para ser igual ao seu nome. Posicione-a no centro da tela, um pouco acima de onde o personagem está. Com a câmera seguindo o jogador, a posição do rótulo será sempre próxima ao mesmo local. Deixe o rótulo oculto por padrão.

Agora, vamos adicionar um script ao rótulo para que ele reaja à mensagem:

# respawned.gd
extends Label

const SHOW_TIME := 2

func _ready() -> void:
	MessageHandler.respawn.connect(_on_respawn)

func _on_respawn(_to: Vector2) -> void:
	show()
	await get_tree().create_timer(SHOW_TIME).timeout
	hide()

Neste caso, não nos importamos com a posição, apenas que o respawn aconteça, mas ainda precisamos do parâmetro definido no callback para que ele possa ser chamado corretamente. Neste caso, ele simplesmente mostra a label (alterando sua propriedade visible para true), aguarda alguns segundos e a oculta novamente. Isso permite que o usuário veja que algo aconteceu além do movimento abrupto.

Observe que esta pode ser uma cena completamente separada e não precisa saber nada sobre o jogador ou o nível para exibir a mensagem. Esse é o principal benefício de usar um gerenciador de mensagens global.

Conclusão

Embora este exemplo simples não precise realmente de um gerenciador de mensagens global, sendo uma única cena e tal, serve como uma ilustração do poder de tal sistema. Em um projeto maior com múltiplas cenas, pode ser difícil e sujeito a erros conseguir referências dos nós para conectar sinais. À medida que o projeto cresce e as coisas se movem, essas conexões podem ser quebradas sem que alguém perceba.

Existem outras maneiras de lidar com casos como este, é claro. O sistema de sinais em Godot é poderoso, mas também simplista, então às vezes pode não ser suficiente. Um exemplo disso é quando você precisa implementar uma fila de eventos, que requer um pouco mais do que apenas conectar e emitir sinais. Se houver interesse, podemos incluir um artigo sobre como implementar uma fila de eventos em Godot. Informe-nos através dos nossos canais de contato.

Você pode acessar o projeto de demonstração construído neste artigo no seguinte repositório: https://github.com/vertexludi/godot-message-handler-demo.


Se você precisa de suporte profissional para seu projeto Godot, fale conosco.

Tags: Godot Dicas Padrões

Este post em outros idiomas: EN-US

Inscreva-se em nossa newsletter

Receba novos posts e atualizações diretamente na sua caixa de entrada.