Efeito Máquina de Escrever no Godot: Texto Letra a Letra

Aprenda o efeito máquina de escrever no Godot 4 com RichTextLabel e visible_ratio. Texto letra a letra, controle de velocidade, som e skip.
Efeito Máquina de Escrever no Godot: Texto Letra a Letra
Poucos detalhes dão tanta personalidade a um jogo quanto o efeito máquina de escrever, aquele texto letra a letra que aparece na caixa de diálogo enquanto um som seco marca cada caractere. Neste tutorial você vai montar um componente reutilizável de caixa de diálogo no Godot 4, com GDScript tipado de verdade, controle de velocidade, som por caractere, opção de pular e um sinal para avisar quando a fala termina.
A boa notícia é que o Godot 4 já entrega quase tudo pronto no RichTextLabel. Não precisamos fatiar strings na mão nem lidar com o pesadelo de cortar uma tag de BBCode no meio. Basta revelar os caracteres aos poucos. Vamos por partes.
Por que RichTextLabel e não Label comum
O Label normal do Godot mostra texto simples e não tem uma forma limpa de revelar caractere por caractere. Já o RichTextLabel tem duas propriedades feitas para isso:
visible_characters: um inteiro que diz quantos caracteres estão visíveis. Vale-1para mostrar tudo.visible_ratio: um float de0.0a1.0que representa a fração do texto visível.
As duas trabalham juntas (mexer numa atualiza a outra). O detalhe genial é que, com bbcode_enabled ligado, essas propriedades contam apenas os caracteres reais do texto e ignoram as tags de BBCode. Ou seja, você pode escrever [wave]Olá, viajante![/wave] que a revelação nunca vai parar no meio de uma tag. É exatamente o comportamento que precisamos para o efeito máquina de escrever.
Comece criando a cena. Estrutura sugerida:
CaixaDialogo (Control)
└─ Painel (PanelContainer)
└─ Texto (RichTextLabel)
└─ SomLetra (AudioStreamPlayer)
No RichTextLabel (nó Texto), marque Bbcode Enabled como ligado no Inspector. Isso é o suficiente para começarmos a programar.
Revelando o texto letra a letra com visible_ratio
A ideia central é simples: definimos o texto completo de uma vez, zeramos a fração visível e vamos aumentando visible_ratio a cada frame usando o delta. O delta garante que a velocidade seja a mesma independente do FPS da máquina do jogador.
Vamos pensar em velocidade como caracteres por segundo. Para converter isso em incremento de visible_ratio, precisamos saber o total de caracteres reais do texto. O RichTextLabel expõe isso em get_total_character_count().
extends Control
## Emitido quando o texto termina de aparecer por completo.
signal texto_finalizado
@export var caracteres_por_segundo: float = 40.0
@onready var texto: RichTextLabel = $Painel/Texto
@onready var som_letra: AudioStreamPlayer = $SomLetra
var _revelando: bool = false
var _total_caracteres: int = 0
var _caracteres_visiveis_anterior: int = 0
func _ready() -> void:
texto.bbcode_enabled = true
texto.visible_ratio = 0.0
Aqui já declaramos o sinal texto_finalizado, a velocidade exportada (dá pra ajustar no Inspector por instância) e algumas variáveis de estado. O _revelando diz se a animação está em andamento, e o _caracteres_visiveis_anterior vai servir para o som, mais adiante.
Agora a função que inicia uma nova fala:
func mostrar(nova_fala: String) -> void:
texto.text = nova_fala
_total_caracteres = texto.get_total_character_count()
texto.visible_characters = 0
_caracteres_visiveis_anterior = 0
_revelando = true
Definimos o texto, guardamos o total de caracteres visíveis (já sem as tags), zeramos a revelação e ligamos o _revelando. Note que atribuímos visible_characters = 0 em vez de visible_ratio = 0.0. Os dois funcionam, mas trabalhar em caracteres inteiros deixa a lógica do som mais precisa depois.
Controlando a velocidade com delta no _process
Com o texto pronto para ser revelado, o _process faz o trabalho pesado. A cada frame ele avança a fração visível proporcionalmente ao tempo decorrido.
func _process(delta: float) -> void:
if not _revelando:
return
texto.visible_ratio += (caracteres_por_segundo / float(_total_caracteres)) * delta
if texto.visible_ratio >= 1.0:
texto.visible_ratio = 1.0
_finalizar_revelacao()
O cálculo caracteres_por_segundo / _total_caracteres transforma a velocidade desejada em quanto da fração precisamos ganhar por segundo. Multiplicando por delta, temos o incremento certo para este frame. Quando visible_ratio alcança ou passa de 1.0, travamos em 1.0 e chamamos a finalização.
Um cuidado: se _total_caracteres for zero (texto vazio), a divisão dá problema. Vale proteger no mostrar, ou simplesmente não chamar com string vazia. Para textos normais, a divisão é segura.
A função de finalização é curta e emite o sinal:
func _finalizar_revelacao() -> void:
_revelando = false
emit_signal("texto_finalizado")
Pronto, já temos o efeito máquina de escrever básico funcionando, com velocidade estável em qualquer FPS e um sinal avisando o fim.
Tocando um som a cada caractere revelado
O som é o que vende a sensação de máquina de escrever. A estratégia é comparar quantos caracteres estão visíveis agora com quantos estavam no frame anterior. Se o número cresceu, tocamos o beep.
Ajuste o _process para conferir isso logo após avançar a revelação:
func _process(delta: float) -> void:
if not _revelando:
return
texto.visible_ratio += (caracteres_por_segundo / float(_total_caracteres)) * delta
_tocar_som_se_avancou()
if texto.visible_ratio >= 1.0:
texto.visible_ratio = 1.0
_finalizar_revelacao()
E a função nova:
func _tocar_som_se_avancou() -> void:
var visiveis_agora: int = texto.visible_characters
if visiveis_agora > _caracteres_visiveis_anterior:
if som_letra.stream != null:
som_letra.play()
_caracteres_visiveis_anterior = visiveis_agora
Como visible_characters conta somente os caracteres reais, o som só dispara em letras de verdade, nunca em tags de BBCode. Um detalhe prático: use um som bem curto (poucos milissegundos) para o beep. Sons longos ficam empilhando e viram um zumbido. Se quiser variar o tom, dá para mexer no som_letra.pitch_scale antes do play() com um valor aleatório pequeno, mas isso já é tempero opcional.
Permitindo pular (skip) o efeito
Ninguém quer reler uma fala inteira em câmera lenta. O padrão consagrado: o primeiro toque no botão de avançar revela tudo de uma vez; o segundo toque passa para a próxima fala. Vamos implementar a parte do skip.
func pular() -> void:
if _revelando:
texto.visible_ratio = 1.0
_finalizar_revelacao()
Simples assim. Se ainda estamos revelando, jogamos visible_ratio para 1.0 e finalizamos, o que também emite o sinal texto_finalizado. Se não estamos revelando (o texto já está completo), a função não faz nada, e cabe ao seu gerenciador de diálogo interpretar esse toque como avançar.
Para ligar isso a uma entrada, capture o input no próprio componente ou deixe o gerenciador decidir. Uma forma direta dentro da caixa:
func _unhandled_input(evento: InputEvent) -> void:
if evento.is_action_pressed("avancar_dialogo"):
if _revelando:
pular()
Crie a ação avancar_dialogo em Project Settings, Input Map (por exemplo mapeada para Espaço ou Enter). Assim, apertar durante a revelação completa o texto na hora. Quando o texto já estiver inteiro, você pode deixar o gerenciador de diálogo tratar o mesmo botão para chamar a próxima fala, evitando que um único toque pule duas coisas ao mesmo tempo.
O cuidado com o BBCode explicado
Vale reforçar por que esse método é confiável com formatação. Imagine a fala:
var fala: String = "Cuidado com o [color=red]dragão[/color] adiante!"
O texto real tem os caracteres de "Cuidado com o dragão adiante!", e as tags [color=red] e [/color] são metadados de formatação, não conteúdo. Quando você usa visible_ratio ou visible_characters, o RichTextLabel conta apenas os caracteres visíveis e aplica a cor assim que o trecho colorido começa a aparecer. Você nunca vê [color=re escrito na tela, porque a engine nunca revela uma tag pela metade. É por isso que preferimos essa abordagem em vez de cortar a string manualmente, onde você teria que rastrear cada tag para não quebrar nada.
Se um dia precisar de efeitos animados no próprio texto, como [wave] ou [shake], tudo continua funcionando junto com a revelação. Para animações mais elaboradas na caixa (a caixa subindo, o retrato do personagem deslizando), vale combinar com animar por código com Tween no Godot, que casa muito bem com este componente.
Juntando tudo: o componente completo
Aqui está o script final da caixa de diálogo, reunindo revelação, velocidade, som, skip e sinal. É um único arquivo pronto para anexar ao nó CaixaDialogo.
extends Control
## Emitido quando o texto termina de aparecer por completo.
signal texto_finalizado
@export var caracteres_por_segundo: float = 40.0
@onready var texto: RichTextLabel = $Painel/Texto
@onready var som_letra: AudioStreamPlayer = $SomLetra
var _revelando: bool = false
var _total_caracteres: int = 0
var _caracteres_visiveis_anterior: int = 0
func _ready() -> void:
texto.bbcode_enabled = true
texto.visible_ratio = 0.0
func mostrar(nova_fala: String) -> void:
texto.text = nova_fala
_total_caracteres = texto.get_total_character_count()
texto.visible_characters = 0
_caracteres_visiveis_anterior = 0
_revelando = _total_caracteres > 0
if not _revelando:
emit_signal("texto_finalizado")
func _process(delta: float) -> void:
if not _revelando:
return
texto.visible_ratio += (caracteres_por_segundo / float(_total_caracteres)) * delta
_tocar_som_se_avancou()
if texto.visible_ratio >= 1.0:
texto.visible_ratio = 1.0
_finalizar_revelacao()
func _tocar_som_se_avancou() -> void:
var visiveis_agora: int = texto.visible_characters
if visiveis_agora > _caracteres_visiveis_anterior:
if som_letra.stream != null:
som_letra.play()
_caracteres_visiveis_anterior = visiveis_agora
func pular() -> void:
if _revelando:
texto.visible_ratio = 1.0
_finalizar_revelacao()
func _finalizar_revelacao() -> void:
_revelando = false
emit_signal("texto_finalizado")
func _unhandled_input(evento: InputEvent) -> void:
if evento.is_action_pressed("avancar_dialogo"):
if _revelando:
pular()
Como usar em uma cena de jogo
Com o componente pronto, chamar uma fala é trivial. Num nó gerenciador, instancie ou referencie a caixa e conecte o sinal:
extends Node
@onready var caixa: Control = $CaixaDialogo
var _falas: Array[String] = [
"Bem-vindo à [color=gold]Vila Aurora[/color].",
"Os monstros voltaram para as [wave]florestas[/wave].",
"Você é nossa única esperança, viajante.",
]
var _indice: int = 0
func _ready() -> void:
caixa.texto_finalizado.connect(_ao_finalizar_texto)
caixa.mostrar(_falas[_indice])
func _ao_finalizar_texto() -> void:
print("Fala completa, aguardando o jogador avançar.")
Quando o jogador terminar de ler e apertar o botão, seu gerenciador chama mostrar com a próxima fala do array. Repare que este componente foca no efeito de texto. Para a estrutura maior, com ramificações, escolhas e personagens, vale evoluir para um sistema de diálogo completo para jogos que use esta caixa como peça de exibição.
Ajustes finos que valem a pena
Alguns detalhes elevam o resultado sem muito esforço:
- Velocidade por fala: expor
caracteres_por_segundojá ajuda, mas você pode aceitar a velocidade como parâmetro emmostrarpara deixar certos personagens falando mais devagar. - Pausas com pontuação: para uma cadência mais natural, dá para reduzir temporariamente a velocidade ao revelar uma vírgula ou ponto, criando micro pausas dramáticas.
- Não tocar som em espaços: se quiser, cheque o caractere recém-revelado e evite o beep em espaços em branco, deixando o efeito menos mecânico.
Nenhum desses é obrigatório, mas todos partem da mesma base que montamos: comparar frames e reagir ao progresso da revelação.
Conclusão
O efeito máquina de escrever no Godot 4 é um daqueles casos em que a engine faz o trabalho difícil por você. Com RichTextLabel, bbcode_enabled e visible_ratio, você revela o texto letra a letra sem quebrar tags, controla a velocidade com delta para manter tudo estável, toca um som a cada caractere, deixa o jogador pular e ainda emite um sinal limpo no fim. O resultado é um componente pequeno, tipado e reutilizável que você pluga em qualquer jogo.
Se você está começando agora e quer entender bem os fundamentos por trás de sinais, _process e nós, considere seguir uma trilha de Godot para iniciantes. Com a base firme, adaptar receitas como esta vira algo natural, e a sua caixa de diálogo passa a ter a cara do seu jogo.
Perguntas frequentes
Como fazer texto aparecer letra a letra no Godot 4?
Use um RichTextLabel com o texto já definido e revele os caracteres aos poucos alterando a propriedade visible_ratio (de 0.0 a 1.0) ou visible_characters no _process, usando o delta para controlar a velocidade.
Qual a diferença entre visible_ratio e visible_characters?
visible_ratio vai de 0.0 a 1.0 e representa a fração do texto visível. visible_characters é um número inteiro de caracteres visíveis. Ambos ignoram as tags de BBCode na contagem, então revelam apenas o texto real.
O efeito máquina de escrever funciona com BBCode ligado?
Sim. Com bbcode_enabled ativo, tanto visible_ratio quanto visible_characters contam apenas os caracteres visíveis do texto, ignorando as tags. Você vê a formatação aparecer junto sem quebrar as tags no meio.
Como deixar o jogador pular o efeito de texto?
Guarde se a animação ainda está rolando em um booleano. Quando o jogador aperta o botão de avançar e o texto não terminou, defina visible_ratio para 1.0 de uma vez e pare a revelação. No segundo toque, avance o diálogo.
Como tocar um som a cada letra do diálogo?
Compare o número de caracteres visíveis do frame atual com o do frame anterior. Se aumentou, chame play() num AudioStreamPlayer com um som curto. Assim um beep toca a cada novo caractere revelado.
Como saber quando o texto terminou de aparecer?
Emita um sinal personalizado (por exemplo texto_finalizado) no momento em que visible_ratio chega a 1.0. Outros nós, como o gerenciador de diálogo, se conectam a esse sinal para liberar o próximo passo.


