Voltar para o Blog
Quest Log

Como Fazer uma Visual Novel no Godot (Guia Para Iniciantes)

Cena de uma visual novel feita no Godot com retrato de personagem e caixa de dialogo

Como fazer uma visual novel no Godot 4: caixa de dialogo, efeito de digitacao, retratos e escolhas que ramificam a historia, em GDScript tipado.

Como Fazer uma Visual Novel no Godot (Guia Para Iniciantes)

Se voce quer aprender a fazer uma visual novel no Godot e nunca se achou bom em programacao, esse genero e o seu melhor ponto de partida. Visual novel e quase toda texto, imagem e escolha, entao a parte de codigo e enxuta e direta. Nesse guia eu vou montar uma do zero na Godot 4, com codigo tipado que de fato roda, explicando cada peca antes de mostrar. No fim voce tera caixa de dialogo, retrato de personagem, efeito de digitacao e escolhas que ramificam a historia.

A ideia aqui nao e te entregar um framework gigante. E te dar uma base pequena, que voce entende inteira, e que da pra expandir no seu projeto sem refatorar tudo depois. Vamos por partes.

Passo 1: a estrutura de cena

Antes de qualquer linha de codigo, monte a cena. Uma visual novel tem quatro elementos visuais basicos: o fundo (background), o retrato do personagem que esta falando, a caixa onde o texto aparece e os botoes de escolha. Tudo isso e UI, entao a raiz da cena vai ser um no Control.

A arvore de nos fica assim:

  • VisualNovel (Control), com o script principal
  • Fundo (TextureRect), a imagem de cenario que ocupa a tela toda
  • Retrato (TextureRect), a imagem do personagem
  • CaixaDialogo (Panel ou ColorRect), o painel da fala
    • NomeFalante (Label), o nome de quem fala
    • TextoFala (RichTextLabel), onde o texto aparece letra por letra
  • Escolhas (VBoxContainer), onde os botoes de escolha vao nascer

Use RichTextLabel no texto da fala, e nao um Label comum. O motivo e a propriedade visible_characters, que so o RichTextLabel tem do jeito que a gente precisa, e ela e o coracao do efeito de digitacao. Deixe a opcao "BBCode Enabled" ligada no inspetor, assim depois voce pode usar negrito e cor no meio das falas.

Com a cena montada, o esqueleto do script principal carrega as referencias dos nos:

extends Control

@onready var fundo: TextureRect = $Fundo
@onready var retrato: TextureRect = $Retrato
@onready var nome_falante: Label = $CaixaDialogo/NomeFalante
@onready var texto_fala: RichTextLabel = $CaixaDialogo/TextoFala
@onready var escolhas: VBoxContainer = $Escolhas

var indice: int = 0
var digitando: bool = false

A variavel indice guarda em que fala da historia voce esta. A variavel digitando diz se o efeito de digitacao ainda esta rolando. Esses dois inteiros e booleanos simples controlam todo o fluxo, como voce vai ver.

Aqui esta a decisao que mais importa pra manter a sanidade: separe a historia do codigo. Em vez de espalhar print e if pelo script, guarde as falas como dados. O jeito mais simples na Godot e um Array de Dictionary, onde cada Dictionary representa uma fala com quem diz (falante) e o que diz (texto).

var roteiro: Array[Dictionary] = [
    { "falante": "Mari", "texto": "Voce finalmente acordou. Achei que ia dormir o dia todo." },
    { "falante": "Voce", "texto": "Que horas sao? Onde eu estou?" },
    { "falante": "Mari", "texto": "Na minha casa. Voce desmaiou na chuva ontem." },
]

Repare que cada item e um Dictionary com duas chaves de texto. Isso ja te da uma vantagem enorme: pra escrever mais historia, voce so adiciona itens ao Array, sem tocar na logica. Um roteirista que nao programa consegue mexer aqui sem medo de quebrar nada. Quando o projeto crescer, da pra migrar essa estrutura pra um Resource do Godot (uma classe com @export), mas pra comecar o Array de Dictionary resolve e e mais facil de ler.

Agora a funcao que mostra a fala do indice atual. Ela le o Dictionary daquela posicao, coloca o nome no Label e dispara o efeito de digitacao no texto:

func mostrar_fala() -> void:
    if indice >= roteiro.size():
        fim_da_historia()
        return

    var fala: Dictionary = roteiro[indice]
    nome_falante.text = fala["falante"]
    digitar(fala["texto"])

A checagem indice >= roteiro.size() no comeco evita o erro classico de tentar ler uma posicao que nao existe quando a historia acaba. Sem ela, o jogo quebra na ultima fala. Acesso as chaves do Dictionary com colchetes (fala["falante"]), que e a forma segura na Godot.

Passo 3: o efeito de digitacao (typewriter)

O efeito de digitacao e o que faz o texto parecer vivo. A tecnica boa nao e concatenar caractere por caractere numa String, e usar visible_characters do RichTextLabel. Voce poe o texto inteiro no Label de uma vez, zera visible_characters e vai aumentando esse numero. O texto ja esta la, voce so controla quantos caracteres ficam visiveis.

Pra cadenciar a revelacao, uso um Timer. Crie um no Timer filho da cena (chame de RelogioDigitacao), deixe o "Wait Time" em torno de 0.03 e conecte o sinal timeout. A cada disparo, revelamos mais um caractere.

@onready var relogio: Timer = $RelogioDigitacao

func digitar(texto: String) -> void:
    texto_fala.text = texto
    texto_fala.visible_characters = 0
    digitando = true
    relogio.start()

func _on_relogio_digitacao_timeout() -> void:
    if texto_fala.visible_characters < texto_fala.get_total_character_count():
        texto_fala.visible_characters += 1
    else:
        relogio.stop()
        digitando = false

A logica e direta: enquanto ainda houver caractere escondido, soma um a visible_characters. Quando chega no total (get_total_character_count), para o Timer e marca digitando como false, sinalizando que a fala terminou de aparecer. O Timer repetindo a cada 0.03 segundo da aquela cadencia de maquina de escrever.

Vale conhecer a alternativa com Tween, que e ainda mais curta. Em vez do Timer, voce anima visible_characters de zero ate o total ao longo de um tempo:

func digitar_com_tween(texto: String) -> void:
    texto_fala.text = texto
    texto_fala.visible_characters = 0
    var total: int = texto_fala.get_total_character_count()
    var animacao: Tween = create_tween()
    animacao.tween_property(texto_fala, "visible_characters", total, total * 0.03)

As duas abordagens funcionam. O Timer te da controle fino pra, por exemplo, tocar um som a cada letra; o Tween e mais enxuto quando voce so quer o efeito. Comece com a que voce achar mais clara de ler.

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

Passo 4: avancar a fala com clique ou tecla

O jogador precisa controlar o ritmo. A regra que todo mundo espera de uma visual novel e: se o texto ainda esta sendo digitado, o primeiro clique completa a fala na hora; se a fala ja esta inteira na tela, o clique avanca pra proxima. Isso respeita tanto quem le rapido quanto quem le devagar.

Capturo isso no _input, reagindo ao clique do mouse ou a tecla de avancar (a acao ui_accept, que ja vem mapeada no Enter e no espaco):

func _input(evento: InputEvent) -> void:
    var avancar: bool = evento.is_action_pressed("ui_accept")
    var clicou: bool = evento is InputEventMouseButton and evento.pressed

    if not (avancar or clicou):
        return

    if digitando:
        completar_fala()
    elif escolhas.get_child_count() == 0:
        proxima_fala()

func completar_fala() -> void:
    relogio.stop()
    texto_fala.visible_characters = texto_fala.get_total_character_count()
    digitando = false

func proxima_fala() -> void:
    indice += 1
    mostrar_fala()

Tem um detalhe importante na condicao do elif: so avanco quando escolhas.get_child_count() == 0, ou seja, quando nao ha botoes de escolha na tela. Se houver escolhas esperando, o jogador tem que clicar num botao, e nao no fundo, pra seguir. Sem essa checagem, dava pra pular as escolhas sem querer e furar a ramificacao. completar_fala para o Timer e revela o texto inteiro de uma vez, que e o comportamento de "pular a digitacao".

Passo 5: escolhas que ramificam a historia

Agora a parte que so o jogo tem: deixar o jogador escolher e mandar a historia pra um lado diferente. A ramificacao na nossa estrutura e simples de entender: cada escolha aponta pra um indice do roteiro. Clicou numa opcao, mudamos indice pra aquele trecho e mostramos a fala de la.

Primeiro, marco no roteiro onde existem escolhas. Adiciono uma fala com uma chave opcoes, que e um Array de Dictionary com o texto do botao e o destino:

var roteiro: Array[Dictionary] = [
    { "falante": "Mari", "texto": "Voce quer ficar mais um pouco ou ja vai?" },
    {
        "falante": "Voce",
        "texto": "Preciso decidir...",
        "opcoes": [
            { "texto": "Vou ficar.", "destino": 3 },
            { "texto": "Tenho que ir.", "destino": 5 },
        ]
    },
    { "falante": "Mari", "texto": "(esta linha nunca e alcancada diretamente)" },
    { "falante": "Mari", "texto": "Que bom. Vou fazer um cha." },
]

Em mostrar_fala, depois de digitar o texto, checo se aquela fala tem opcoes. Se tiver, monto os botoes; se nao, o fluxo segue pelo clique normal. Os botoes nascem na hora dentro do VBoxContainer:

func mostrar_fala() -> void:
    if indice >= roteiro.size():
        fim_da_historia()
        return

    var fala: Dictionary = roteiro[indice]
    nome_falante.text = fala["falante"]
    digitar(fala["texto"])

    if fala.has("opcoes"):
        montar_escolhas(fala["opcoes"])

func montar_escolhas(opcoes: Array) -> void:
    for filho in escolhas.get_children():
        filho.queue_free()

    for opcao in opcoes:
        var botao: Button = Button.new()
        botao.text = opcao["texto"]
        var destino: int = opcao["destino"]
        botao.pressed.connect(func() -> void: ir_para(destino))
        escolhas.add_child(botao)

func ir_para(destino: int) -> void:
    for filho in escolhas.get_children():
        filho.queue_free()
    indice = destino
    mostrar_fala()

Tem um cuidado em montar_escolhas que evita um bug chato. Copio opcao["destino"] pra uma variavel local destino antes de usar na funcao do connect. Se voce usar opcao["destino"] direto dentro da lambda, todos os botoes podem acabar apontando pro mesmo destino quando forem clicados. Guardar numa variavel local fixa o valor certo de cada botao. E o tipo de erro que custa uma tarde inteira se voce nao souber que existe.

O ir_para faz o pulo: limpa os botoes, troca indice pelo destino da escolha e chama mostrar_fala de novo. Como indice agora aponta pra outro trecho do Array, a historia continua por aquele caminho. E so isso. Ramificacao numa visual novel e, no fundo, mudar qual posicao do roteiro voce esta lendo.

Por fim, a funcao que fecha tudo quando a historia acaba:

func fim_da_historia() -> void:
    texto_fala.text = "Fim."
    nome_falante.text = ""
    relogio.stop()

func _ready() -> void:
    mostrar_fala()

O _ready chama mostrar_fala uma vez pra exibir a primeira linha, e a partir dai o clique do jogador conduz o resto.

Trocando fundo e retrato durante a historia

Uma visual novel fica viva quando o cenario e o personagem mudam junto com a fala. Da pra fazer isso com a mesma estrutura de dados: adicione chaves opcionais no Dictionary da fala apontando pra imagem, e troque a textura quando elas existirem.

func aplicar_visual(fala: Dictionary) -> void:
    if fala.has("fundo"):
        fundo.texture = load(fala["fundo"])
    if fala.has("retrato"):
        retrato.texture = load(fala["retrato"])

Chame aplicar_visual(fala) dentro de mostrar_fala, logo depois de pegar a fala. Agora uma linha do roteiro pode trazer "fundo": "res://cenas/quarto.png" e a imagem troca sozinha quando aquela fala aparece. Como as chaves sao opcionais (so trocam se existirem), as falas que nao mudam de cenario seguem usando o ultimo fundo, que e exatamente o que voce quer.

Por que a Godot e otima pra esse genero

Tudo que voce viu aqui sao recursos nativos da Godot 4: nos de UI, RichTextLabel com visible_characters, Timer, Tween, Array e Dictionary. Voce nao precisou de plugin nem de biblioteca externa. A engine e gratuita, roda em maquina modesta e exporta pra desktop, web e celular, que e justamente onde visual novel se da bem.

Se o seu jogo crescer e a parte de conversa ficar complexa, vale estudar um sistema de dialogo mais robusto, com historico e condicoes, do tipo que eu detalho no guia de sistema de dialogo para jogos. E se o que te atrai mesmo e a historia, o post sobre narrativa em jogos mostra como fazer as escolhas pesarem de verdade, indo alem do codigo. Os dois conversam direto com o que voce montou aqui.

Por onde seguir

Nao tente fazer a visual novel inteira de uma vez. O caminho que funciona e crescer em camadas, e cada camada que voce viu aqui ja roda sozinha:

  1. Monte a cena e mostre uma fala fixa na tela.
  2. Ligue o Array de Dictionary e o efeito de digitacao.
  3. Adicione o avancar com clique e tecla.
  4. So entao coloque as escolhas e a ramificacao.
  5. Por ultimo, troca de fundo e retrato, som e transicoes.

Cada passo desses te deixa com algo jogavel, nunca com um monte de codigo pela metade que nao roda. Esse e o jeito de terminar projeto.

Se voce quer ir alem do basico e dominar a engine pra fazer jogos maiores depois, vale investir num caminho estruturado: eu reuni o que considero o melhor curso de Godot pra quem esta comecando do absoluto zero. Mas pra hoje, abra a Godot, monte essa cena e coloque a sua primeira fala pra rodar. Visual novel e o genero que mais recompensa quem comeca pequeno.

Perguntas frequentes

Preciso saber programar muito pra fazer uma visual novel?

Nao. Visual novel e um dos generos mais amigaveis pra quem esta comecando. O grosso do trabalho e organizar texto e imagens, e o codigo que move tudo isso cabe em poucas dezenas de linhas. Se voce entende variavel, Array e funcao, ja consegue montar a sua.

Como faco o texto aparecer letra por letra?

Use um RichTextLabel e a propriedade visible_characters. Voce coloca o texto inteiro no Label de uma vez, comeca com visible_characters em zero e vai aumentando esse numero com um Timer. Cada disparo do Timer revela mais um caractere, criando o efeito de digitacao.

Como organizo os dialogos da visual novel?

O caminho mais simples e um Array de Dictionary, onde cada Dictionary guarda quem fala e o que fala. Assim a historia fica separada do codigo, e voce edita as falas sem mexer na logica. Quando o projeto cresce, da pra migrar esses dados para um Resource do Godot.

Como crio escolhas que mudam a historia?

Cada escolha e um botao que aponta para um indice diferente do roteiro. Ao clicar, voce muda o indice atual da fala para o trecho daquela ramificacao e mostra a proxima linha. E so um pulo controlado dentro do Array de falas.

A Godot e boa pra fazer visual novel?

Sim. A Godot 4 tem tudo que uma visual novel precisa: nos de UI prontos, RichTextLabel com formatacao, Timer, Tween e um sistema de cena leve. E gratuita, roda em maquinas modestas e exporta para varias plataformas. E um otimo primeiro projeto pra aprender a engine.