Voltar para o Blog
Quest Log

Legendas em Jogo Godot: Sistema de Subtitles Acessível

Tela de jogo com uma caixa de legenda semitransparente exibindo a fala de um personagem e a marcação [porta rangendo]

Aprenda a criar um sistema de legendas em jogo Godot 4 com foco em acessibilidade: falas, narração, legendas de SFX, contraste e sincronização com o áudio.

Se você quer que mais gente jogue o que você faz, legendas em jogo Godot não são um extra: são o que permite que pessoas surdas, com deficiência auditiva ou jogando no ônibus sem fone acompanhem a sua história. A parte boa é que o sistema de base é simples de montar na Godot 4. A parte que quase todo mundo ignora são as boas práticas de acessibilidade que fazem a legenda ser de fato legível. Este post cobre as duas coisas: um SubtitleManager que roda no seu projeto hoje e as decisões de contraste, tamanho e sincronização que separam uma legenda útil de uma que atrapalha.

Já existe por aqui um material sobre a camada de conceito em design inclusivo e jogos acessíveis. O que faltava era a implementação técnica. É isso que vamos resolver agora.

O que um sistema de legendas precisa fazer

Antes do código, vale saber o que estamos construindo. Uma legenda de jogo cobre três tipos de conteúdo:

  • Falas: o que os personagens dizem, geralmente com o nome de quem fala.
  • Narração: texto sem um personagem visível na tela.
  • Legendas de SFX: descrições de sons importantes para quem não os escuta, escritas entre colchetes, como [porta rangendo] ou [tiro ao longe].

Esse terceiro tipo é o que muita gente esquece. Uma pessoa surda que só recebe as falas perde metade da informação do jogo: não sabe que tem um inimigo se aproximando pelas costas, que a chuva começou, que aquele barulho era uma armadilha armando. Legendar SFX relevantes é o que torna o jogo jogável de verdade sem áudio.

Do lado técnico, o sistema precisa de:

  • Um lugar central para exibir texto, disponível de qualquer cena.
  • Uma fila, para que uma legenda não apague a anterior antes da hora.
  • Duração calculada pelo tamanho do texto, não um valor fixo.
  • Configurações que o jogador controla: ligar/desligar e tamanho da fonte.

O nó de legenda: Label dentro de uma caixa com contraste

A regra de acessibilidade mais importante da UI de legenda é contraste. Texto branco puro sobre uma cena clara desaparece. A solução padrão da indústria é uma caixa semitransparente escura atrás do texto, que garante legibilidade sobre qualquer fundo.

Monte a cena Subtitle.tscn assim:

  • Um CanvasLayer (para ficar sempre por cima do jogo).
  • Um PanelContainer centralizado embaixo, com um StyleBoxFlat de cor preta e alpha por volta de 0.6.
  • Um Label dentro, com autowrap_mode ligado, horizontal_alignment centralizado e uma fonte com outline.

O outline (contorno) reforça o contraste mesmo quando a caixa não é suficiente. Na Godot 4 você configura isso no tema ou direto no Label:

extends PanelContainer

@onready var label: Label = $MarginContainer/Label

func _ready() -> void:
    label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
    label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
    label.add_theme_font_size_override("font_size", 32)
    label.add_theme_color_override("font_color", Color.WHITE)
    label.add_theme_color_override("font_outline_color", Color.BLACK)
    label.add_theme_constant_override("outline_size", 6)

Deixe a caixa com uma largura máxima em torno de 70% da tela e nunca deixe o texto passar de duas linhas. Legenda com quatro linhas ninguém lê a tempo. Se a fala é longa, quebre em blocos e mostre um de cada vez pela fila (chegamos lá).

O SubtitleManager como autoload

O coração do sistema é um autoload. Registre um script SubtitleManager.gd em Project > Project Settings > Autoload para que qualquer cena possa chamar SubtitleManager.show_subtitle(...) sem precisar de referências.

Ele instancia a cena Subtitle.tscn, controla o Label e o Timer, e mantém a fila:

extends Node

const SUBTITLE_SCENE := preload("res://ui/Subtitle.tscn")
const MIN_DURATION := 1.5
const SECONDS_PER_CHAR := 0.05

var _subtitle: PanelContainer
var _label: Label
var _timer: Timer
var _fila: Array[Dictionary] = []
var _mostrando: bool = false

var legendas_ligadas: bool = true
var tamanho_fonte: int = 32

func _ready() -> void:
    _subtitle = SUBTITLE_SCENE.instantiate()
    add_child(_subtitle)
    _label = _subtitle.get_node("MarginContainer/Label")
    _subtitle.hide()

    _timer = Timer.new()
    _timer.one_shot = true
    add_child(_timer)
    _timer.timeout.connect(_ao_terminar)

    _carregar_config()

func show_subtitle(texto: String, duracao: float = 0.0) -> void:
    if not legendas_ligadas or texto.is_empty():
        return
    if duracao <= 0.0:
        duracao = _calcular_duracao(texto)
    _fila.append({"texto": texto, "duracao": duracao})
    if not _mostrando:
        _proxima()

func _proxima() -> void:
    if _fila.is_empty():
        _mostrando = false
        _subtitle.hide()
        return
    _mostrando = true
    var item: Dictionary = _fila.pop_front()
    _label.add_theme_font_size_override("font_size", tamanho_fonte)
    _label.text = item["texto"]
    _subtitle.show()
    _timer.start(item["duracao"])

func _ao_terminar() -> void:
    _proxima()

func _calcular_duracao(texto: String) -> float:
    return max(MIN_DURATION, texto.length() * SECONDS_PER_CHAR)

Repare no _calcular_duracao. A legenda fica na tela por tempo proporcional ao número de caracteres, com um piso de 1,5 segundo. Um [clique] some rápido; uma fala longa fica o suficiente para ler. Isso é muito melhor do que um valor fixo que ou corre demais ou trava o ritmo.

A fila (_fila) resolve o problema clássico: dois sons quase simultâneos disparam duas legendas, e sem fila a segunda apagaria a primeira instantaneamente. Aqui elas entram em sequência, cada uma com sua duração.

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

Legenda de fala: nome de quem fala

Para falas, prefixe o nome do personagem. Isso ajuda quem não distingue as vozes a saber quem está falando, e é trivial de fazer. Uma pequena função de conveniência mantém o padrão:

func show_fala(personagem: String, texto: String) -> void:
    show_subtitle("%s: %s" % [personagem, texto])

Se o seu jogo já tem uma camada de conversa, dá para plugar o SubtitleManager direto no fluxo do seu sistema de diálogo, disparando uma legenda a cada linha exibida na caixa de fala. Assim as legendas nascem de graça a partir do texto que você já escreveu.

Legenda de SFX sincronizada com o áudio

Aqui está o ponto central da acessibilidade. Toda vez que você toca um som narrativamente relevante, dispare a legenda dele no mesmo instante. A chave é chamar show_subtitle no mesmo frame do play():

extends Node2D

@onready var som_porta: AudioStreamPlayer = $SomPorta

func abrir_porta() -> void:
    som_porta.play()
    SubtitleManager.show_subtitle("[porta rangendo]")

Como o play() e o show_subtitle acontecem juntos, a legenda aparece junto com o som. Para quem ouve, o texto e o áudio batem. Para quem não ouve, a informação chega mesmo assim.

Nem todo som precisa de legenda. Legendar cada passo ou cada tiro polui a tela. A régua é: o som carrega informação que o jogador precisa? Uma porta rangendo às suas costas, sim. O barulho de fundo ambiente, não. Escolha os sons que mudam a decisão do jogador.

Para efeitos ambientes que se repetem, evite reenfileirar a mesma legenda várias vezes. Um controle simples de "última legenda de SFX" resolve:

var _ultimo_sfx: String = ""
var _ultimo_sfx_tempo: float = 0.0

func show_sfx(texto: String) -> void:
    var agora := Time.get_ticks_msec() / 1000.0
    if texto == _ultimo_sfx and agora - _ultimo_sfx_tempo < 1.0:
        return
    _ultimo_sfx = texto
    _ultimo_sfx_tempo = agora
    show_subtitle(texto)

Se a fonte do som é uma pista posicional importante (algo à sua direita, por exemplo), considere indicar a direção no próprio texto: [passos à direita]. Áudio direcional é uma das informações que a pessoa surda mais perde, e uma palavra resolve. Se você quer entender melhor como organizar os canais de som que vão alimentar essas legendas, vale ver como funciona a mixagem com audio bus na Godot.

Opções do jogador: ligar/desligar e tamanho da fonte

Acessibilidade é dar controle. Duas opções são obrigatórias: um interruptor de legendas e o tamanho da fonte. Salve tudo em um ConfigFile, que persiste entre sessões:

const CONFIG_PATH := "user://acessibilidade.cfg"

func _carregar_config() -> void:
    var cfg := ConfigFile.new()
    if cfg.load(CONFIG_PATH) == OK:
        legendas_ligadas = cfg.get_value("legendas", "ligadas", true)
        tamanho_fonte = cfg.get_value("legendas", "tamanho_fonte", 32)

func salvar_config() -> void:
    var cfg := ConfigFile.new()
    cfg.set_value("legendas", "ligadas", legendas_ligadas)
    cfg.set_value("legendas", "tamanho_fonte", tamanho_fonte)
    cfg.save(CONFIG_PATH)

func set_tamanho_fonte(novo: int) -> void:
    tamanho_fonte = clamp(novo, 24, 56)
    salvar_config()

func set_legendas(ativo: bool) -> void:
    legendas_ligadas = ativo
    if not ativo:
        _fila.clear()
        _subtitle.hide()
        _mostrando = false
    salvar_config()

No seu menu de opções, ligue um CheckButton em set_legendas e um OptionButton ou HSlider em set_tamanho_fonte. Ofereça pelo menos pequeno, médio e grande. O piso de 24 px e o teto de 56 px do clamp evitam extremos que quebram o layout. Como o show_subtitle já aplica tamanho_fonte a cada legenda, a mudança vale na hora seguinte, sem reiniciar nada.

Um detalhe que faz diferença: teste o seu jogo com as legendas ligadas e o som desligado por completo. Se você consegue jogar e entender o que está acontecendo, uma pessoa surda também consegue. Se você se perde, faltou legendar algum som.

Boas práticas que valem mais que o código

O sistema acima é a base. O que separa uma legenda profissional é o cuidado:

  • Contraste sempre. Caixa escura semitransparente mais outline no texto. Nunca confie só na cor da fonte sobre a cena.
  • No máximo duas linhas. Fala longa vira blocos na fila.
  • Tempo proporcional ao texto. Nunca duração fixa para tudo.
  • Nome de quem fala. Barato e ajuda muito.
  • SFX relevantes, não todos. Legende o que muda a decisão do jogador.
  • Direção quando importa. [explosão à esquerda] vale a palavra extra.
  • Deixe o jogador ajustar. Ligar/desligar e tamanho são o mínimo.

Nenhuma dessas coisas é difícil. Elas só precisam de intenção desde o começo, em vez de virar remendo no fim do projeto.

Fechando

Com um SubtitleManager como autoload, uma caixa com contraste e a disciplina de disparar a legenda junto com o som, você cobre falas, narração e SFX de uma vez. É um sistema que cabe em qualquer projeto Godot 4 e que amplia de verdade quem consegue jogar o que você faz.

Se você quer levar acessibilidade a sério no seu jogo e não parar só nas legendas, comece pela base de design inclusivo e vá plugando cada sistema com esse mesmo cuidado. Um som de cada vez, uma legenda de cada vez.

Perguntas frequentes

Como fazer legendas em jogo na Godot 4?

Crie um autoload SubtitleManager com um Label dentro de um Panel semitransparente e um Timer. A função show_subtitle recebe o texto e a duração, exibe o Label e agenda o Timer para escondê-lo. Uma fila garante que legendas não se atropelem.

Como incluir legendas de efeitos sonoros para pessoas surdas?

Dispare a legenda no mesmo momento em que toca o AudioStreamPlayer, usando uma marcação entre colchetes como "[porta rangendo]" ou "[passos se aproximando]". Isso descreve o som para quem não o ouve e é uma prática recomendada de acessibilidade.

Qual o tamanho de fonte ideal para legendas?

Comece em torno de 32 px em 1080p e deixe o jogador aumentar. Ofereça pelo menos as opções pequeno, médio e grande, salvas em um ConfigFile, e nunca deixe a legenda estourar duas linhas.

Como sincronizar a legenda com o áudio na Godot?

Chame show_subtitle no mesmo frame em que dá play no AudioStreamPlayer e calcule a duração pelo número de caracteres do texto, com um mínimo. Para falas longas, quebre em blocos e use a fila do SubtitleManager.