Os últimos seis artigos construíram as peças: o loop, os nós, a física, a arquitetura, o som, a interface e o save. Este artigo as junta num jogo acabado e percorre o caminho que todo jogo faz antes de chegar a alguém: decidir o que ele é, ajustar a dificuldade com dados, testar com pessoas, organizar o projeto e exportá-lo para várias plataformas, até a publicação numa loja.
Um documento de design de uma página
Jogos grandes têm documentos de design de centenas de páginas. Um jogo pequeno precisa de uma página, e precisa dela: sem uma descrição escrita do que o jogo é, cada ajuste vira uma discussão, e cada ideia nova parece boa. O documento da "Biblioteca em Apuros":
Premissa. Uma estante gigante desabou. Os livros caem do alto, e a bibliotecária precisa apanhá-los antes que toquem o chão.
Loop principal (10 segundos). Ver um livro cair → correr ou pular até ele → apanhar → ouvir e ver a recompensa → repetir, cada vez mais rápido.
Loop de sessão (2 a 4 minutos). Construir uma sequência longa para multiplicar os pontos, arriscar pulos por livros raros, desviar das traças, e perder quando as vidas acabarem.
Loop de longo prazo. Bater o próprio recorde. No capstone: subir no ranking público da Livraria.
Regras. Livro comum: 10 pontos. Livro raro: 50 pontos, cai mais rápido. Multiplicador: +1 a cada 10 livros seguidos, até 5×. Livro perdido: −1 vida e zera a sequência. Traça: −1 vida, sem zerar a sequência. Marcador de página: +1 vida, até o máximo de 3.
Controles. Mover, pular, pausar. Teclado, controle e toque.
Plataformas. Windows, Linux, macOS, Android.
Fora do escopo desta versão. Fases, chefes, loja de itens, multijogador.
A última linha é a mais importante. A lista do que o jogo não é protege o projeto do crescimento sem fim, que é a principal razão pela qual jogos independentes não chegam a ser publicados. Toda ideia nova ("e se houvesse um chefe dragão de papel?") é comparada com essa lista, e a maioria vai para uma versão futura.
A regra de que a traça não zera a sequência é um exemplo de decisão de design que só fica clara quando escrita. Perder a sequência por um livro que caiu é consequência de uma falha na tarefa principal. Perder a sequência também por um inimigo tornaria cada encontro com uma traça um desastre desproporcional, e o jogador passaria a fugir das traças em vez de apanhar livros. O documento registra o porquê, e as próximas discussões começam dali.
A dificuldade como dado
Nos artigos anteriores, a dificuldade era uma fórmula no código: 1 + tempo / 30. Linear, crescendo para sempre. Testando com jogadores, dois problemas aparecem: os primeiros 20 segundos são entediantes para quem já jogou, e depois de três minutos o jogo fica impossível, e não difícil. A curva certa não é uma reta, e ajustá-la recompilando o código a cada tentativa é lento demais.
O Godot tem um tipo de recurso que resolve isso: a Curve, uma curva editável graficamente no Inspetor. Um recurso próprio agrupa todas as curvas do jogo:
[GlobalClass]
public partial class ConfiguracaoDaPartida : Resource
{
[Export] public float DuracaoDeReferencia { get; set; } = 240f; // segundos até o topo da curva
[ExportGroup("Chuva de livros")]
[Export] public Curve IntervaloEntreLivros { get; set; } = null!; // segundos, pelo progresso
[Export] public Curve VelocidadeDosLivros { get; set; } = null!; // px/s
[Export] public Curve ChanceDeRaro { get; set; } = null!; // 0 a 1
[ExportGroup("Traças")]
[Export] public Curve IntervaloEntreTracas { get; set; } = null!;
[Export(PropertyHint.Range, "0,1")] public float ChanceDeMarcador { get; set; } = 0.03f;
public float Progresso(float tempo) => Mathf.Clamp(tempo / DuracaoDeReferencia, 0f, 1f);
public float Intervalo(float tempo) => IntervaloEntreLivros.Sample(Progresso(tempo));
public float Velocidade(float tempo) => VelocidadeDosLivros.Sample(Progresso(tempo));
public bool SortearRaro(float tempo, RandomNumberGenerator s) => s.Randf() < ChanceDeRaro.Sample(Progresso(tempo));
}
O recurso é salvo como um arquivo .tres e atribuído à cena principal. As curvas são desenhadas no editor: o intervalo entre livros começa em 1,1 s, cai rápido até 0,6 s no primeiro minuto, depois desce devagar até um piso de 0,3 s. A velocidade sobe em degraus suaves. A chance de raro fica em 5% no início e sobe para 15% no fim, recompensando quem sobrevive. Sample devolve o valor da curva num ponto entre 0 e 1.
Três vantagens aparecem de imediato. Quem ajusta a dificuldade não precisa programar, e testa a mudança apertando F5. Várias configurações podem coexistir: facil.tres, normal.tres e desafio_do_dia.tres são arquivos diferentes, e escolher o modo de jogo é escolher o recurso. E o .tres é texto versionado no Git, e as mudanças de balanceamento aparecem nos pull requests como qualquer outra mudança.
A separação de "Arquitetura de jogo e por que não alocar dentro do loop" continua valendo. RegrasDaPartida não depende de Curve, que é um tipo do Godot. Ela recebe uma interface com os valores de que precisa, e a ConfiguracaoDaPartida é adaptada para essa interface na cena. Os testes usam uma implementação simples em C#.
Os últimos ingredientes
Com a estrutura dos artigos anteriores, o que falta é conteúdo, e cada item novo se encaixa sem mexer no que existe:
- O marcador de página: uma
Area2Dcom um script de cinco linhas, que emite um sinal ao ser apanhado. Usa a camada "itens" do exercício 2 de "Input, física e colisões" e sai do pool de itens. - Os primeiros dez segundos como tutorial: em vez de uma tela de instruções que ninguém lê, os primeiros livros caem devagar, bem perto da bibliotecária, e um texto discreto ("← → para andar", "Espaço para pular") aparece e some quando o jogador executa a ação. O primeiro livro raro só aparece depois do primeiro pulo. O jogo ensina pelas situações.
- O fim de jogo: a tela congela por um instante, a pontuação final sobe com um contador animado, e se for recorde, um campo de nome aparece com o foco já nele, para quem joga com teclado.
- Toque na tela, para Android: dois botões virtuais transparentes nos cantos inferiores e um toque na metade superior para pular. Os botões virtuais disparam as mesmas ações do mapa de entrada com
Input.ActionPress("mover_esquerda"). A lógica da bibliotecária não sabe se a entrada veio do teclado, do controle ou do vidro.
Organização do projeto
BibliotecaEmApuros/
├── project.godot
├── BibliotecaEmApuros.csproj
├── cenas/
│ ├── principal.tscn
│ ├── menu.tscn
│ ├── entidades/ bibliotecaria.tscn, livro.tscn, traca.tscn, marcador.tscn
│ └── interface/ hud.tscn, pausa.tscn, opcoes.tscn, fim_de_jogo.tscn
├── scripts/
│ ├── entidades/
│ ├── componentes/ ComponenteDeVida.cs, CaixaDeDano.cs, CaixaDeGolpe.cs
│ ├── sistemas/ SonsDoJogo.cs, CameraDoJogo.cs, PoeiraDePaginas.cs
│ ├── autoloads/ Fluxo.cs, Eventos.cs, EstadoDoJogo.cs
│ └── infraestrutura/ PoolDeNos.cs, Salvamento.cs, Opcoes.cs
├── recursos/ partida_normal.tres, tema.tres, traducoes.csv
├── sprites/
├── audio/ efeitos/ (wav), musica/ (ogg)
├── Regras/ biblioteca .NET sem Godot
└── Testes/ xUnit sobre Regras
As cenas e os scripts de uma mesma entidade poderiam ficar juntos numa pasta por entidade, que é outra convenção comum no Godot. O importante é escolher uma e manter. As convenções de nome também contam: cenas e recursos em snake_case, que é o padrão do Godot, e classes C# em PascalCase, que é o padrão do .NET. O .gitignore exclui .godot/ (o cache de importação, regenerado pelo editor) e as pastas bin/ e obj/ do .NET.
Testar com pessoas
Nenhum dos testes automatizados diz se o jogo é divertido. Isso só se descobre olhando outras pessoas jogarem. O método, usado de estúdios pequenos a grandes, é simples e desconfortável:
- Entregar o jogo sem explicar nada.
- Observar em silêncio. Não ajudar. A vontade de dizer "é só apertar espaço" é enorme, e cada vez que se cede a ela, perde-se a informação de que o jogo não ensinou aquilo.
- Anotar onde a pessoa hesita, onde erra, onde sorri, onde desiste.
- Perguntar depois, com perguntas abertas: "o que você achou que devia fazer quando a traça apareceu?", e não "a traça ficou clara?".
Nas primeiras sessões da "Biblioteca em Apuros", três observações mudaram o jogo:
- Quase ninguém percebia o multiplicador. A pontuação subia mais rápido e ninguém sabia por quê. O multiplicador ganhou um indicador grande ao lado da pontuação, que pulsa e muda de cor a cada nível, e um som ascendente ao subir.
- Os jogadores morriam para traças que surgiam da borda da tela a centímetros da bibliotecária. Passaram a surgir com um aviso de meio segundo: uma sombra piscando na borda por onde vão entrar.
- Livros raros caindo exatamente no ponto oposto ao da bibliotecária eram frustrantes, não desafiadores. A posição de nascimento dos raros passou a ter uma distância máxima da posição atual dela.
Nenhuma dessas mudanças teria aparecido nos testes automatizados, e as três vieram de ver pessoas jogando.
Para complementar a observação, o jogo grava, localmente e só em versões de teste, uma linha por partida num arquivo de log: duração, pontos, causa da derrota (livro perdido ou traça), maior sequência e dificuldade alcançada. Vinte partidas de cinco pessoas já mostram padrões: se 80% das partidas terminam por traças, a traça está forte demais para o papel que o documento de design lhe deu.
Desempenho: a máquina mais modesta
Antes de exportar, o jogo é testado na máquina mais fraca que se pretende suportar, e não no computador de desenvolvimento. Para a "Biblioteca em Apuros", um notebook antigo com gráficos integrados e um celular Android de entrada. O profiler do Godot (aba Depurador → Profiler) mostra o tempo de cada função por quadro, e os Monitores mostram quadros por segundo, memória, objetos e nós na árvore. O MonitorDeMemoria do artigo de arquitetura de jogo confirma que o loop não aloca. A meta é simples: 60 quadros estáveis nas duas máquinas, com margem, na dificuldade máxima.
No celular, dois ajustes costumam ser necessários: o renderizador Compatibilidade (baseado em OpenGL ES 3, mais leve e com suporte mais amplo que o renderizador Móvel baseado em Vulkan) e texturas comprimidas pelo formato próprio de cada GPU, que o Godot gera na exportação.
Exportando
A exportação no Godot depende de modelos de exportação (export templates), os executáveis pré-compilados do motor para cada plataforma, baixados uma vez pelo editor na versão exata do motor. Para projetos C#, é preciso a versão .NET dos modelos.
Em Projeto → Exportar, cada plataforma ganha um preset:
| Preset | Formato | Observações |
|---|---|---|
| Windows | .exe + .pck (ou .pck embutido) |
ícone e metadados de versão no executável |
| Linux | binário x86_64 + .pck |
testar numa distribuição limpa |
| macOS | .app em .zip ou .dmg |
assinatura e notarização da Apple |
| Android | .aab para a Play Store, .apk para testes |
SDK do Android, JDK e keystore de assinatura |
O .pck é o pacote com todas as cenas, scripts compilados, texturas e sons, em formato otimizado. Para projetos C#, a exportação publica o projeto .NET junto, com o runtime autocontido: o jogador não precisa ter o .NET instalado, como no caixa de "O caixa da Livraria em Avalonia".
Os filtros do preset decidem o que entra: a pasta Testes/, arquivos de exemplo e ferramentas de desenvolvimento ficam de fora. A opção de depuração fica desligada nas exportações de lançamento: builds de depuração são mais lentos e incluem ferramentas que o jogador não deve ter.
A exportação também roda pela linha de comando, sem abrir o editor, e isso permite automatizá-la:
godot --headless --export-release "Windows" build/windows/BibliotecaEmApuros.exe
godot --headless --export-release "Linux" build/linux/BibliotecaEmApuros.x86_64
godot --headless --export-release "Android" build/android/BibliotecaEmApuros.aab
Num pipeline de CI, como o de "Deploy: contêineres, nginx, Linux e a orquestração com Aspire", cada tag de versão dispara: os testes das regras com dotnet test, a exportação para todas as plataformas numa imagem de contêiner com o Godot e os modelos instalados, e o envio dos pacotes para as lojas. Existem imagens mantidas pela comunidade com o Godot pronto para CI. As chaves de assinatura (o keystore do Android, os certificados da Apple) ficam nos segredos do pipeline, nunca no repositório.
Assinatura e confiança
Cada sistema tem sua forma de desconfiar de executáveis desconhecidos:
- Windows: um
.exesem assinatura digital aciona o aviso do SmartScreen ("o Windows protegeu o computador"). Um certificado de assinatura de código resolve com o tempo, conforme o arquivo ganha reputação. - macOS: aplicativos precisam ser assinados com um certificado de desenvolvedor da Apple e notarizados (enviados à Apple para verificação automática). Sem isso, o sistema se recusa a abri-los por padrão.
- Android: todo pacote é assinado. A Play Store usa o esquema de assinatura pelo Google, em que o desenvolvedor mantém uma chave de envio. Perder a chave de envio tem recuperação. Perder a chave de um app assinado fora desse esquema significa nunca mais poder atualizá-lo.
Versão e registro de erros
O número de versão (1.0.0) fica nas configurações do projeto e aparece no menu, discreto, num canto. Quando um jogador relata um problema, a primeira pergunta é "qual versão?", e a resposta precisa estar à vista.
Exceções não tratadas em scripts C# no Godot são registradas no log e não fecham o jogo, e o nó que falhou simplesmente para de funcionar naquele quadro. Isso evita travamentos, mas pode esconder problemas. Como o motor captura essas exceções, elas não chegam a AppDomain.CurrentDomain.UnhandledException: aparecem no log do motor, que no desktop o Godot já grava em user://logs/. O tratador de UnhandledException continua útil para o que roda fora das chamadas do motor, como threads próprias, e grava no mesmo lugar. Uma opção "Copiar informações de diagnóstico" no menu de opções coloca na área de transferência a versão, o sistema e as últimas linhas do log, pronta para colar num relato de erro.
Publicando
Para um primeiro jogo, o itch.io é a porta de entrada mais simples: criar a página, enviar os pacotes e definir o preço (inclusive "pague quanto quiser"). A ferramenta de linha de comando butler envia versões novas enviando só as diferenças, e se integra ao CI:
butler push build/windows gkult/biblioteca-em-apuros:windows --userversion 1.0.0
butler push build/linux gkult/biblioteca-em-apuros:linux --userversion 1.0.0
A Steam exige uma taxa por jogo, a integração com o Steamworks (conquistas, nuvem, sobreposição da loja, feita em C# com bibliotecas como Steamworks.NET ou Facepunch.Steamworks) e um processo de revisão. A Google Play exige uma conta de desenvolvedor, a política de privacidade publicada e a classificação etária por questionário. Cada loja tem seus requisitos de capturas de tela, vídeos e textos, e preparar esse material costuma levar mais tempo do que se imagina.
Lista de verificação antes do lançamento
- [ ] O jogo abre, joga e fecha nas máquinas e aparelhos mais modestos que se pretende suportar.
- [ ] 60 quadros estáveis na dificuldade máxima.
- [ ] Teclado, controle e toque funcionam do menu ao fim de jogo, sem mouse.
- [ ] Remapeamento de controles, volumes, tela cheia, idioma e "reduzir movimento" funcionam e são lembrados.
- [ ] O save sobrevive a fechar o jogo à força durante a gravação. O save da versão anterior é migrado.
- [ ] Nenhum texto fixo fora das traduções. Os textos longos cabem nos dois idiomas.
- [ ] Builds de lançamento sem depuração, assinados, com a versão visível.
- [ ] Créditos com todas as licenças de fontes, sons e músicas usados.
- [ ] Pelo menos cinco pessoas que não participaram do desenvolvimento jogaram a versão final.
A "Biblioteca em Apuros" está pronta: um documento de design que diz o que o jogo é e o que não é, uma curva de dificuldade ajustada com dados e testada com pessoas, um tutorial que ensina jogando, e pacotes assinados para Windows, Linux, macOS e Android, gerados por um pipeline e publicados numa loja. Com isso a Fase 9 termina. Em sete artigos, o C# saiu de um laço com Stopwatch no console e chegou a um jogo completo, passando pelo MonoGame, pelo Godot, pela física, pela arquitetura e pelo cuidado com cada byte alocado no loop. A Livraria agora tem banco, API, site, caixa, aplicativo e um jogo. Falta dar um passo atrás para olhar o conjunto: como organizar sistemas desse tamanho para que continuem compreensíveis e mudáveis por anos.
Fontes e leituras recomendadas
- Godot Engine — Exporting projects e Exporting for Android: https://docs.godotengine.org/en/stable/tutorials/export/exporting_projects.html
- Godot Engine — Command line tutorial (exportação sem editor): https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html
- Godot Engine — Resources e Curve: https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html
- Jesse Schell — The Art of Game Design: A Book of Lenses, 3ª edição (CRC Press, 2019).
- Celia Hodent — The Gamer's Brain: How Neuroscience and UX Can Impact Video Game Design (CRC Press, 2017), sobre testes com jogadores.
- itch.io — butler documentation: https://itch.io/docs/butler/
Exercícios
Exercício 1
Um membro da equipe propõe acrescentar "um chefe no fim de cada minuto: uma traça gigante que precisa ser acertada com livros". Use o documento de design para avaliar a proposta e redija a resposta.
Ver resposta
✓ Resposta: O documento diz que "fases e chefes" estão fora do escopo desta versão, e o loop principal é apanhar livros, não acertar inimigos. A proposta introduz uma mecânica nova (arremessar livros), que exige controles novos, uma regra nova sobre o destino dos livros apanhados (guardados? arremessados?), animações, balanceamento e testes, e compete com a tarefa principal: enquanto o jogador mira no chefe, os livros caem. Resposta sugerida: "A ideia é boa e pode render uma versão 2. Para a 1.0, fica fora, porque muda o loop principal e o documento exclui chefes. Vou registrá-la na lista de ideias futuras com o que ela exigiria: arremesso, mira, e uma regra para os livros durante o confronto. Se quisermos algo no espírito dela ainda nesta versão, um evento de 'enxame de traças' a cada minuto, sem mecânica nova, cabe no escopo e pode ser testado com jogadores." O documento transforma a discussão sobre gosto numa discussão sobre escopo.
Exercício 2
Desenhe, em palavras, a curva IntervaloEntreLivros que atende a estas observações dos testes: os primeiros 20 s são entediantes para veteranos; ao redor de 2 min o ritmo deve ser intenso mas possível; depois de 3 min o jogo deve continuar ficando mais difícil, sem se tornar impossível.
Ver resposta
✓ Resposta: Com DuracaoDeReferencia = 240 s, o progresso 0 corresponde a 0 s e o progresso 1 a 4 min. Pontos da curva: em 0 (início), 1,0 s entre livros, suficiente para o tutorial, mas não arrastado; em 0,08 (cerca de 20 s), 0,8 s, uma queda rápida inicial para que veteranos cheguem logo ao ritmo; em 0,5 (2 min), 0,5 s, o ritmo intenso, com a inclinação da curva já diminuindo; em 0,75 (3 min), 0,38 s; em 1,0 (4 min), 0,32 s, uma aproximação assintótica de um piso. Depois de 4 min, o Clamp do progresso mantém 0,32 s, e a dificuldade continua a subir por outras curvas (velocidade dos livros, frequência de traças) mais lentamente, para que ela não se torne impossível de uma vez. A forma é uma exponencial decrescente: descida rápida no começo e aproximação de um piso no fim. Um intervalo que chegasse a zero tornaria o jogo impossível por definição.
Exercício 3
Nos testes, um jogador com controle não conseguiu digitar o nome na tela de recorde. Proponha duas soluções.
Ver resposta
✓ Resposta: (1) Teclado virtual na tela: uma grade de letras navegável com o direcional, com "apagar" e "confirmar", como os jogos de fliperama e de console fazem. O LineEdit recebe as letras escolhidas. Em consoles e no Steam Deck, o sistema oferece um teclado virtual próprio, que pode ser acionado pela integração com a plataforma. (2) Nome padrão e opcional: o campo já vem preenchido com o último nome usado (salvo nas preferências) ou com "Jogador", e o botão "Confirmar" recebe o foco. Quem joga com controle confirma com um botão, e quem quer mudar o nome pode. Muitos jogos combinam as duas coisas. O erro original foi pressupor um teclado físico, o mesmo que a lista de verificação ("do menu ao fim de jogo, sem mouse") tenta impedir.
Exercício 4
O .aab do Android foi rejeitado pela Play Store por "pacote não assinado com a chave de envio esperada", e a máquina que tinha o keystore foi formatada. O que isso significa e o que deveria ter sido feito?
Ver resposta
✓ Resposta: Com a assinatura de apps pelo Google Play, a loja guarda a chave de assinatura do aplicativo, e o desenvolvedor assina os envios com uma chave de envio separada. Perdida a chave de envio, é possível pedir ao Google que registre uma nova, pelo console da Play, com um processo de verificação. O aplicativo continua atualizável, mas com atraso. Se o app não estivesse no esquema do Google e a chave perdida fosse a chave de assinatura do próprio app, nenhuma atualização jamais poderia ser publicada com o mesmo identificador de pacote, e os jogadores teriam de instalar outro app do zero, perdendo os dados. O que deveria ter sido feito: guardar o keystore e a senha num cofre de segredos da equipe, com cópias em pelo menos dois lugares independentes; usar a assinatura pelo Google Play desde o primeiro envio; e ter o pipeline de CI como o único lugar que assina, lendo a chave dos segredos, e nunca uma máquina pessoal.
Exercício 5
Escolha três itens da lista de verificação de lançamento e diga como automatizar cada um no pipeline de CI, e um que não pode ser automatizado.
Ver resposta
✓ Resposta: (a) Migração do save: guardar no repositório arquivos de save de cada versão publicada (saves/v1.json, saves/v2.json) e um teste xUnit que carrega cada um com Salvamento.Carregar e verifica os dados esperados. Uma versão nova que quebre a leitura de saves antigos falha o build. (b) Textos fora das traduções: um script que percorre os .tscn e procura propriedades text que não sejam chaves de tradução (fora de uma lista de exceções), e falha se encontrar alguma, e outro que verifica se todas as chaves do CSV têm valor em todos os idiomas. (c) Build de lançamento assinado e com versão: a exportação com --export-release a partir da tag Git, com a versão injetada nas configurações do projeto, e uma etapa final que verifica a assinatura dos pacotes (signtool verify no Windows, apksigner verify no Android). Não automatizável: "cinco pessoas que não participaram do desenvolvimento jogaram a versão final". Diversão, clareza e frustração só aparecem observando pessoas. Testes automatizados dizem se o jogo funciona. Pessoas dizem se ele vale a pena.