Capítulo 01 Básico
Serious games e o porquê da web
Um jogo que ensina só cumpre a missão se chegar ao aluno e se sobrar alguma evidência de que ele aprendeu. A web resolve o primeiro problema e cria dois novos.
Este guia trata de um fluxo específico e muito comum em educação corporativa, escolas, saúde e treinamento técnico: um jogo feito em Unity, publicado como build Web, entregue por um LMS (ou por um link) e capaz de devolver dados sobre o que o jogador fez. Cada elo desse fluxo tem armadilhas próprias, e a maioria delas só aparece quando o jogo já está pronto.
1.1 O que é um serious game
O termo vem de Clark Abt, que em Serious Games (1970) descreveu jogos com finalidade educacional explícita e cuidadosa, e não apenas de entretenimento. Michael & Chen (2005) popularizaram a expressão para jogos digitais que educam, treinam e informam. A fronteira com termos vizinhos costuma confundir:
| Termo | O que é | Exemplo |
|---|---|---|
| Gamification | Elementos de jogo (pontos, níveis, ranking) aplicados a uma atividade que não é um jogo | Curso em vídeo com medalhas por módulo |
| Serious game | Um jogo completo, desenhado para um propósito além do entretenimento | Simulador de evacuação de emergência |
| Game-based learning (GBL) | Uso de jogos (sérios ou comerciais) como método de ensino, com objetivos de aprendizagem definidos | Aula que usa um jogo de estratégia para discutir logística |
| Simulação | Modelo de um sistema real para prática; pode ou não ter regras de jogo | Simulador de cirurgia ou de voo |
Se você quer aprofundar a diferença entre essas categorias, veja Gamification. Aqui o foco é técnico: colocar o jogo no ar e medir.
1.2 Por que publicar na web
Zero instalação
Escolas e empresas costumam ter computadores bloqueados, Chromebooks e políticas que impedem instalar executáveis. Um link abre em qualquer navegador moderno.
Integra com o LMS
Moodle, Canvas, Blackboard e plataformas corporativas sabem executar conteúdo web empacotado (SCORM) e registrar conclusão e nota.
Atualização instantânea
Corrigiu um erro no enunciado de uma fase? Publicou a nova versão e todos jogam a correção, sem pedir que ninguém reinstale nada.
Alcance e evidência
Um link no portfólio ou no e-mail do treinamento é jogado em segundos. E como tudo passa pela web, é natural enviar dados de uso a um servidor.
O navegador impõe limites de tamanho (o jogador espera o download antes de jogar), de memória (a aba pode ser encerrada), de recursos (sem threads, sem sockets) e de contexto (o jogo roda dentro de um iframe do LMS, com regras de segurança próprias). Os capítulos 2 a 8 tratam desses limites; os capítulos 9 a 11 tratam dos dados.
1.3 O fluxo completo, do editor ao dado
1. Unity (projeto) → build Web (cap. 2)
2. Build enxuta: código, assets, compressão (caps. 4 a 7)
3. Carregamento inteligente: Addressables, loading, memória (cap. 8)
4. Empacotamento: SCORM (cap. 9) ou hospedagem própria + cmi5/xAPI (cap. 10)
5. LMS / link → aluno joga
6. Eventos → LRS ou LMS → análise (cap. 11)
Essa combinação aparece em vagas e projetos de desenvolvedor de serious games, learning engineer / analista de learning analytics, desenvolvedor de EdTech, treinamento corporativo e compliance, simulação em saúde e segurança e museus e comunicação científica. Quem domina só Unity entrega um jogo; quem domina o pipeline inteiro entrega um jogo que a organização consegue distribuir, rastrear e provar que funciona. Para a carreira em Unity em si, veja Unity e Game Design.
Classifique cada caso como gamification, serious game, GBL ou simulação, e justifique com a pergunta "a atividade principal é um jogo?": (a) app de idiomas com sequência diária e ranking; (b) jogo de gerenciar uma UTI em que decisões clínicas erradas têm consequências; (c) turma jogando Minecraft para discutir urbanismo; (d) treinamento de segurança em que você percorre uma fábrica virtual e marca riscos.
Ver gabarito
(a) Gamification: a atividade principal é estudar; o jogo é uma camada. (b) Serious game (com componente de simulação): a atividade principal é jogar, com propósito clínico. (c) GBL: um jogo comercial usado como método, com objetivo de aprendizagem definido pelo professor. (d) Simulação, que vira serious game se tiver regras, metas e feedback de jogo. Fronteiras assim são graduais; o importante é saber qual delas você está construindo.
Escreva para um jogo que você tenha ou queira fazer: quem é o aluno, em que dispositivo e rede ele vai jogar (escola? casa? celular?), qual LMS ou canal entrega o jogo, e qual decisão alguém tomará com os dados. Guarde essa ficha: os capítulos 4, 7 e 11 vão usá-la.
Ver exemplo resolvido
"Alunos do 9º ano, em Chromebooks da escola, com Wi-Fi compartilhado (~5 Mbps por turma). Entrega via Moodle da rede municipal. Decisão: a professora quer saber em quais conceitos de fração a turma mais erra, para escolher o tema da aula seguinte." Repare que o contexto (rede fraca, Moodle) já dita orçamento de tamanho e formato de empacotamento.
Capítulo 02 Básico
Unity na web: o que sai do build e o que não existe
Antes de otimizar, conheça a peça. Uma build Web é um pequeno site: cada arquivo tem um papel e um custo.
2.1 WebGL, WebAssembly e o nome da plataforma
A build Web da Unity roda no navegador combinando duas tecnologias: WebGL 2 (a API gráfica do navegador) e WebAssembly (o código do seu jogo e do motor, compilado via IL2CPP para um binário compacto). Nas versões mais antigas a plataforma se chamava WebGL; no Unity 6 ela aparece como Web, nos Build Profiles. Neste guia, "build Web" e "build WebGL" significam a mesma coisa. Os nomes exatos de menus e opções mudam entre versões: use este texto como mapa e confirme na documentação da sua versão.
2.2 O que sai da pasta Build
| Arquivo | Papel | Costuma pesar |
|---|---|---|
index.html | Página que cria o canvas e chama o loader. É o template que você pode personalizar. | Poucos KB |
Build/*.loader.js | Carrega os demais arquivos, mostra progresso, cria a instância do jogo (createUnityInstance). | Dezenas de KB |
Build/*.framework.js | Código JavaScript de suporte do runtime (Emscripten) e dos plugins .jslib. | Centenas de KB |
Build/*.wasm | O código: motor Unity + seus scripts C# convertidos por IL2CPP. | A maior parte do peso "de código" |
Build/*.data | Os assets empacotados: cenas, texturas, malhas, áudio, shaders. | A maior parte do peso "de conteúdo" |
StreamingAssets/ | Arquivos copiados como estão, lidos por UnityWebRequest. Não entram no .data. | Depende do que você colocou |
TemplateData/ | Logo, barra de progresso, favicon e CSS do template padrão. | Pequeno |
Com compressão ligada (cap. 7), os arquivos da pasta Build ganham sufixo .gz ou .br. O servidor precisa então avisar o navegador de que estão comprimidos, o que é a causa mais frequente de "a build não abre depois de publicar".
2.3 O que muda em relação ao desktop
| Recurso | Na build Web | O que fazer |
|---|---|---|
Threads (System.Threading, Jobs em paralelo) | Não há threads reais na configuração padrão (verifique o status na sua versão). | Projete sem depender de paralelismo; use corrotinas e trabalho em fatias por frame. |
| Rede | Sem sockets nem System.Net de baixo nível; só o que o navegador oferece. | Use UnityWebRequest (HTTP) e, se precisar de tempo real, WebSocket via plugin .jslib. |
| Arquivos | System.IO opera num sistema de arquivos virtual, em memória; a persistência vai para o IndexedDB do navegador. | Não confie em arquivo local para guardar progresso importante (cap. 8). |
| Compute shaders, HDRP | Não suportados no WebGL 2. | Use Built-in ou URP e efeitos simples. |
| Áudio, tela cheia, bloqueio do cursor | O navegador só libera após um gesto do usuário (clique, toque, tecla). | Desenhe uma tela inicial "Toque para começar"; ela também ajuda no áudio. |
| Coleta de lixo (GC) | Só roda entre frames, não durante a execução de código gerenciado. | Evite alocações repetidas em laços; pré-aloque, use StringBuilder e NativeArray. |
Application.Quit | Não fecha a aba. | Ofereça "Voltar ao curso" por outro mecanismo (cap. 9). |
Tudo isso funciona no Editor, que é um desktop. Um jogo que "roda perfeitamente" no Editor pode falhar na primeira vez que roda num navegador. Faça uma build Web na primeira semana do projeto, não na última, e teste sempre a build, não só o Editor.
Em entrevistas e revisões de código de jogos web, a pergunta "o que você não pode usar no WebGL?" é um filtro clássico. Saber a lista da tabela acima, com o motivo, mostra que você já teve um jogo rejeitado por um erro desses.
Crie um projeto novo (2D ou 3D), uma cena vazia, mude a plataforma para Web e faça a build. Anote o tamanho de cada arquivo da pasta Build (com compressão Gzip e depois Disabled). Você acabou de medir o "peso zero" do motor; todo o resto do projeto se soma a ele.
Pegue a ficha do exercício 1.2 e marque quais itens da tabela 2.3 ameaçam o seu jogo (por exemplo: você usa threads? salva arquivo local? depende de áudio ao iniciar?). Escreva a mitigação de cada um em uma linha.
Capítulo 03 Intermediário · Interativo
Anatomia do carregamento: para onde vai o tempo
"O jogo demora a abrir" não é um diagnóstico. O tempo se divide em cinco estágios, e cada um tem um remédio diferente.
3.1 Os cinco estágios
Do clique no link até o primeiro frame jogável, acontece sempre a mesma sequência. Role o bloco abaixo: o painel escuro fica fixo e destaca o estágio que cada cartão descreve.
Download: o custo que você controla pelo tamanho
Vale para todo mundo, mas dói na rede fraca. Reduzir bytes (caps. 5 e 6), comprimir bem (cap. 7) e usar uma CDN atacam este estágio. Na visita seguinte o cache do navegador pode zerá-lo.
Descompressão: o erro de configuração mais comum
Com o cabeçalho Content-Encoding correto, o navegador descomprime em código nativo, rápido. Com o servidor mal configurado, o jogo ou falha ou usa o decompressor JavaScript, mais lento e mais pesado (cap. 7).
Compilação do WebAssembly: pesa em celulares
Quanto maior o .wasm, mais trabalho. Servir o arquivo como application/wasm permite compilar enquanto ele ainda baixa. Este é o motivo de cortar código do motor (cap. 5) ajudar duas vezes: menos bytes e menos compilação.
Dados: o que baixa vira memória da aba
O conteúdo do .data é expandido em memória. Um .data enorme pode estourar a aba em celulares mesmo depois de baixado. O caching de dados evita baixar de novo, mas não evita desempacotar (cap. 8).
A primeira cena é a sua vitrine
Cena inicial leve, shaders pré-aquecidos e carregamento assíncrono do resto fazem o jogo parecer rápido. Percepção de espera também é design (cap. 8).
3.2 Uma tabela de remédios
| Sintoma | Estágio provável | Primeira hipótese |
|---|---|---|
| Barra de progresso demora, depois abre bem | 1 (download) | Build grande demais para a rede; falta CDN; sem cache |
| Erro de "não foi possível ler o arquivo comprimido" ao abrir | 2 (descompressão) | Servidor sem Content-Encoding para .br/.gz |
| Barra chega ao fim e a tela fica parada | 3 ou 4 | Compilação do wasm em CPU fraca ou desempacotar .data grande |
| Abre, mas trava nos primeiros segundos de jogo | 5 | Compilação de shaders, cena inicial pesada, GC |
| Aba fecha ou "Out of memory" | 4 / memória | .data gigante, texturas grandes, memória inicial mal calibrada |
3.3 Como medir de verdade
- Aba Network do DevTools (com "Disable cache" ligado): mostra o tamanho transferido de cada arquivo, o cabeçalho
Content-Encodinge o tempo. Use um perfil de rede lenta para simular escola ou celular. - Aba Performance: grava a carga inteira e mostra a compilação do WebAssembly e o trabalho da CPU.
- Aba Application → IndexedDB: confirma se o caching de dados está guardando algo.
- Marcos próprios: registre
performance.now()quando a instância termina de criar e quando a primeira tela interativa aparece. O que interessa ao aluno é o tempo até poder jogar, não o tempo até o download acabar.
Relatar "reduzi o tempo até jogar de 38 s para 11 s em rede de 5 Mbps, cortando o wasm em 40%" é o tipo de resultado que separa um portfólio de jogos web de um portfólio de projetos que só abrem na máquina do autor.
Publique a build do exercício 2.1 em qualquer hospedagem, abra o DevTools na aba Network, limpe o cache e recarregue com rede lenta. Identifique: qual arquivo é o maior? o navegador recebeu Content-Encoding? quanto tempo passou entre o fim do download e o primeiro frame?
Ver o que esperar
Numa build vazia, o .wasm costuma ser o maior arquivo. Se Content-Encoding aparecer como br ou gzip, o servidor está certo. O intervalo entre o fim do download e o primeiro frame é a soma dos estágios 2 a 5: em máquinas rápidas é curto; em celulares fracos costuma ser bem visível.
Para cada relato, aponte o estágio e a primeira coisa que você verificaria: (a) "abre em 5 s na escola, mas leva 1 minuto na casa da aluna"; (b) "no iPhone a página recarrega sozinha depois de carregar"; (c) "o jogo abre e trava por 4 s ao entrar na primeira fase".
Ver gabarito
(a) Estágio 1: a rede mudou, o resto é igual; reduza bytes e use CDN. (b) Estágio 4 / memória: a aba estourou o limite e o navegador a recarregou; reduza .data e texturas, calibre a memória inicial (cap. 8). (c) Estágio 5: compilação de shaders ou cena inicial pesada; pré-aqueça shaders e carregue a fase de forma assíncrona.
Capítulo 04 Intermediário · Interativo
Orçamento de tamanho e como medir
Otimizar sem meta vira coleção de truques. Defina quantos megabytes o seu aluno pode esperar e meça cada mudança contra esse número.
4.1 Da rede do aluno ao orçamento
A conta do estágio de download é simples: segundos ≈ megabytes × 8 ÷ megabits por segundo. Ela ignora descompressão e compilação, então é um piso, não uma previsão. Use a calculadora para testar o tamanho da sua build contra conexões típicas.
| Conexão de referência | Velocidade | Espera só de download |
|---|---|---|
| Wi-Fi escolar dividido | 5 Mbps | — |
| 4G razoável | 12 Mbps | — |
| Banda larga doméstica | 50 Mbps | — |
Se você informar vários alunos simultâneos, a velocidade de cada linha é dividida entre eles: é o cenário de uma turma inteira abrindo o jogo ao mesmo tempo, que costuma ser o pior caso real de uma escola.
4.2 Faixas de orçamento (uma sugestão de partida)
Não existe um limite oficial. As faixas abaixo são um ponto de partida para a primeira jogada, e você deve ajustá-las com a ficha do exercício 1.2:
| Tamanho transferido | Espera a 5 Mbps | Postura recomendada |
|---|---|---|
| até ~10 MB | ~16 s | Aceitável para a maior parte dos públicos, inclusive escolas |
| ~10 a 30 MB | ~16 a 48 s | Exige tela de loading cuidadosa e um bom motivo (assets únicos) |
| acima de ~30 MB | > 48 s | Divida: build inicial pequena e o resto sob demanda (Addressables, cap. 8) |
4.3 O peso do "vazio"
Uma medição pública de Aras Pranckevičius (engenheiro da Unity) num projeto vazio do Unity 6 mostra a ordem de grandeza do que o motor sozinho custa: o template 3D com URP passou de 10 MB comprimidos (cerca de 3,7 MB de dados e 6,9 MB de código), enquanto o mesmo tipo de projeto, com Built-in, código otimizado, exceções desligadas, sem Input System nem Unity UI e com a compressão Brotli, ficou em torno de 2 MB. O que mais pesou na redução foi remover pacotes que puxam código de motor mesmo sem uso. Os números mudam a cada versão; o aprendizado é que o "vazio" já pode consumir a maior parte do seu orçamento se você aceitar os padrões.
4.4 Como ver o que pesa
- Build Report no Editor.log: depois de cada build, o log do Editor traz um relatório com o tamanho por categoria (texturas, malhas, áudio, shaders, scripts, DLLs incluídas) e os maiores assets. É o primeiro lugar a olhar.
- Pasta Build: compare o tamanho dos arquivos comprimidos, que é o que trafega. O tamanho no Editor.log é antes da compressão do transporte.
- Web Stripping Tool e análise do wasm: ferramentas da Unity para enxergar o quanto do motor está entrando (cap. 5).
# tamanhos dos arquivos comprimidos que realmente trafegam
cd Builds/Web/Build
ls -lh
du -sh .
4.5 O hábito: diário de builds
Mude uma coisa por vez e registre o resultado. Uma planilha de cinco colunas basta:
| Build | Mudança | .wasm | .data | Total transferido |
|---|---|---|---|---|
| #01 | Baseline (padrões da Unity) | — | — | — |
| #02 | Managed Stripping Level: High | — | — | — |
Sem o diário, você não sabe qual mudança valeu a pena e nem qual quebrou o jogo.
Um orçamento de tamanho por escrito ("primeira jogada ≤ 12 MB") é um requisito não funcional que times profissionais tratam como qualquer outro: entra na definição de pronto e é verificado a cada build (cap. 12). É o que evita descobrir o problema na véspera da entrega ao cliente.
Use a calculadora com a rede e o número de alunos da ficha do exercício 1.2. Defina a espera máxima tolerável (ex.: 20 s) e derive o tamanho máximo em MB. Esse número é o seu orçamento.
Ver exemplo resolvido
Turma de 30 alunos num link de 60 Mbps compartilhado: cada aluno tem ~2 Mbps. Espera máxima aceitável: 30 s. MB = 30 × 2 ÷ 8 = 7,5 MB. Se o jogo tem 25 MB, o projeto precisa de Addressables (cap. 8) ou de uma estratégia de cache/pré-carregamento antes da aula.
Faça uma build do seu projeto, abra o Editor.log, ache o Build Report e liste os três assets que mais pesam. Para cada um, escreva se ele é necessário na primeira jogada.
Capítulo 05 Aplicado
Reduzir o código: stripping, IL2CPP e exceções
O .wasm é o arquivo que mais pesa em projetos pequenos, e é o que mais atrapalha em celulares. Cortar código de motor que você não usa rende em dobro.
5.1 De onde vem o peso do código
- O motor: cada subsistema (física, animação, UI, áudio, vídeo, partículas) entra no wasm se algo o referenciar, inclusive por dependência de pacote.
- O seu C#: o IL2CPP converte tudo em C++ e depois em WebAssembly. Genéricos, LINQ e reflexão aumentam o código gerado.
- Bibliotecas gerenciadas: DLLs de terceiros e pacotes entram inteiros ou quase, dependendo do stripping. Um pacote "inocente" pode custar centenas de KB.
5.2 A ordem certa de ataque
Comece pelos ganhos baratos e seguros, e deixe para o fim os que trazem risco de quebrar o jogo:
| # | Ação | Onde (varia por versão) | Risco |
|---|---|---|---|
| 1 | Desligar Development Build em builds de entrega (não comprimido e sem minificação) | Build Profiles / Build Settings | Baixo |
| 2 | Remover pacotes e módulos não usados (ex.: Input System, Unity UI, vídeo, física 3D em jogo 2D, terreno, tecido) | Package Manager (inclusive a aba de módulos nativos) | Baixo a médio: confira se algo do jogo dependia deles |
| 3 | Strip Engine Code ligado | Player Settings → Other Settings | Baixo |
| 4 | Managed Stripping Level em Medium ou High | Player Settings → Other Settings → Optimization | Médio: pode remover código usado só por reflexão |
| 5 | Code Optimization orientado a tamanho (por exemplo, "Disk Size", ou "Disk Size with LTO" para a versão final) | Build Profiles / Build Settings da plataforma Web | Baixo, mas o build demora mais |
| 6 | IL2CPP Code Generation em "Faster (smaller) builds" | Player Settings → Other Settings | Baixo: a execução pode ficar um pouco mais lenta |
| 7 | Enable Exceptions em "None" na versão final | Player Settings → Publishing Settings | Alto: veja o alerta abaixo |
Com exceções em None, o jogo não consegue capturar exceções: um erro que antes era registrado e ignorado pode encerrar a execução. Só desligue depois de testar todas as fases. Durante o desenvolvimento, use uma opção com stack trace para conseguir diagnosticar.
5.3 Stripping sem quebrar: link.xml e [Preserve]
O linker da Unity remove o que parece não usado. Quem carrega tipos por reflexão, por serialização (JSON, por exemplo) ou por AssetBundles/Addressables pode ter uma classe removida e receber um erro só na build. Duas defesas:
<!-- Assets/link.xml -->
<linker>
<assembly fullname="Assembly-CSharp">
<type fullname="MeuJogo.Perguntas.PerguntaData" preserve="all"/>
</assembly>
</linker>
using UnityEngine.Scripting;
[Preserve] // impede o stripping desta classe
public class PerguntaData { public string enunciado; public string[] opcoes; public int correta; }
Regra prática: suba o nível de stripping e teste o jogo inteiro, com foco em tudo o que é lido de arquivo, rede ou dado dinâmico. Sempre que aparecer um erro de tipo ou método ausente só na build, preserve o tipo e registre no diário de builds.
5.4 Ferramentas para enxergar o motor
A Unity oferece o pacote Web Stripping Tool, que analisa o código do motor incluído no WebAssembly e permite remover submódulos que o seu projeto não usa (por exemplo, partes de gráficos 3D num jogo 2D). O fluxo é instalar o pacote, perfilar a build para ver o que é usado e configurar o stripping por submódulo. É uma otimização de segunda etapa, para quando os passos 1 a 7 já foram feitos.
Em portfólios, mostrar uma tabela "antes e depois" do wasm (com o que você mudou em cada linha) prova capacidade de investigar e de trabalhar com restrições. É o tipo de conteúdo que serve tanto para vagas de Unity quanto para as de engenharia de performance web.
Partindo do baseline do exercício 2.1, aplique as ações 1 a 6 da tabela, uma por vez, e registre o tamanho do .wasm após cada uma. Qual ação rendeu mais? Alguma não mudou nada?
Ver o que costuma acontecer
O maior ganho costuma vir de remover pacotes e módulos que puxavam código de motor (ação 2); em seguida vêm Strip Engine Code e o nível de stripping. O Code Optimization por tamanho ajuda, mas o efeito varia por projeto. Se uma ação "não mudou nada", é um sinal de que o código que ela cortaria já não estava lá: registre mesmo assim.
Crie uma classe lida apenas por JSON e sem nenhuma referência direta no código. Suba o Managed Stripping Level para High, faça a build e veja o que acontece. Depois conserte com [Preserve] e com link.xml, e compare.
Capítulo 06 Aplicado
Reduzir os assets: texturas, áudio, malhas, fontes e shaders
O .data é onde o conteúdo do jogo vive, e também o que vira memória da aba. Em serious games, a maior parte do peso costuma ser textura e áudio.
6.1 Texturas: o maior alvo
- Max Size por plataforma: na aba de plataforma Web do Inspector da textura, reduza o tamanho máximo. Uma textura de 2048 px ocupa quatro vezes a de 1024 px. Pergunte sempre: em que tamanho ela aparece na tela?
- Formato de compressão: navegadores de desktop usam DXT; navegadores móveis usam ASTC. Se o formato escolhido não é suportado pelo dispositivo, a Unity usa uma descompressão por software em execução, o jogo ainda roda, mas usa mais memória e pode ficar mais lento. A documentação mostra como gerar um arquivo de dados por formato e escolher o correto com JavaScript no template; se isso for complexo demais para o seu caso, escolha o formato do seu público principal (escolas com PCs e Chromebooks: DXT).
- Crunch: compressão adicional que reduz bastante o tamanho em disco. O custo é um build mais demorado e alguma perda de qualidade. A documentação da Unity a recomenda para reduzir o tamanho da distribuição.
- Mipmaps: desligue em interface e em 2D sem redução de escala (economiza ~33% de memória por textura).
- Read/Write Enabled: mantenha desligado, senão a Unity guarda uma cópia extra na memória.
- Sprite Atlas: agrupe sprites de interface para reduzir chamadas de desenho e desperdício de espaço.
6.2 Áudio
- Comprima (Vorbis) e reduza a qualidade onde ninguém ouve a diferença; efeitos curtos em mono.
- Efeitos curtos podem usar "Decompress On Load"; músicas longas não: descomprimidas, ocupam muito espaço em memória. Prefira "Compressed In Memory" e teste, porque o comportamento de streaming varia entre plataformas.
- Narração e trilhas longas são bons candidatos a carregamento sob demanda (cap. 8), ou até a um servidor de mídia, em vez de irem no
.data. - O áudio só começa depois de um gesto do usuário (cap. 2), então a tela "Toque para começar" é obrigatória, não estética.
6.3 Malhas e animações
- Modele com poucos vértices; simplifique no Blender antes de exportar. Em serious games, clareza pesa mais que realismo.
- No importador do modelo: ative Mesh Compression, desligue Read/Write, não importe o que não usa (câmeras, luzes, materiais embutidos duplicados) e evite tangentes se o material não usa normal map.
- Ative a compressão de animação e remova curvas que não são usadas.
6.4 Shaders
- Cada combinação de palavras-chave gera uma variante de shader, e o total pode explodir. Use shaders simples e limpe a lista de shaders sempre incluídos e as variantes desnecessárias nas configurações de gráficos.
- A compilação de shaders na primeira vez que aparecem causa engasgos no estágio 5 (cap. 3). Use
ShaderVariantCollection.WarmUp()durante a tela de loading para adiantar esse custo.
6.5 Fontes, vídeo e a pasta Resources
- TextMesh Pro: gere atlas estáticos só com os caracteres necessários (letras, números, pontuação e acentos do português: ãõçáéíóúâêôà). Uma fonte com milhares de glifos (CJK, por exemplo) pesa muito.
- Vídeo: na build Web, o
VideoPlayerreproduz por URL, não a partir de um clipe incluído no projeto. Hospede o vídeo em servidor de mídia ou CDN e referencie a URL; não o coloque no.data. - Pasta
Resources/: tudo o que está dentro entra na build, mesmo que nada use. Evite-a; use referências diretas e Addressables (veja também Unity Essencial). - Duplicatas: a mesma textura em dois lugares vira dois assets. O Build Report ajuda a achar.
Times de produção separam orçamento por categoria (ex.: texturas até 40%, áudio 25%, malhas 15%...) e o artista de arte técnica é quem faz cumprir. Se você trabalha sozinho, escreva os limites: "nenhuma textura acima de 1024 px sem justificativa; nenhuma música acima de 1 MB".
No Build Report do exercício 4.2, liste as 5 texturas mais pesadas. Para cada uma: em que tamanho aparece na tela, qual o Max Size atual e qual deveria ser, e se precisa de mipmaps. Aplique as mudanças e refaça a build.
Ver exemplo resolvido
Uma imagem de fundo de 4096×4096 mostrada em tela cheia (1920×1080) pode ir para 2048 sem perda visível: de 16 MP para 4 MP, cerca de 4 vezes menos memória e peso. Um ícone de 512 px exibido a 64 px pode ir para 128 px. Fundos 2D e interface normalmente dispensam mipmaps.
Com o orçamento do exercício 4.1, escreva quantos MB cabem em cada categoria (texturas, áudio, malhas, código, outros) e verifique quais estão estourando.
Capítulo 07 Aplicado
Compressão, servidor e cache
Metade dos jogos que "não abrem depois de publicar" tem a mesma causa: o servidor não diz ao navegador que os arquivos estão comprimidos.
7.1 As opções de compressão da Unity
| Opção | O que faz | Quando escolher |
|---|---|---|
| Gzip | É o padrão. Suportado por todos os navegadores em HTTP e HTTPS. Tamanho médio; build rápido. | Uso geral e desenvolvimento |
| Brotli | Arquivos bem menores que o Gzip, mas o build de release demora muito mais. Chrome e Firefox só o usam em HTTPS. | Versão final, em HTTPS, quando você controla o servidor |
| Disabled | Sem compressão dos arquivos. O servidor ou a CDN pode comprimir na hora. | Hospedagens que comprimem sozinhas; LMS que não deixam definir cabeçalhos (cap. 9) |
| Decompression Fallback (opção à parte, desligada por padrão) | Embute um decompressor em JavaScript na build, que assume quando o navegador não descomprime. Funciona sem configurar o servidor, mas deixa o carregador maior e a descompressão mais lenta. | Quando você não consegue configurar os cabeçalhos |
| Name Files As Hashes | Nomeia os arquivos pelo hash do conteúdo, o que invalida o cache automaticamente a cada nova versão. | Sempre que for usar cache longo (7.4) |
7.2 O que o servidor precisa dizer
Para cada arquivo comprimido, o servidor deve responder com dois cabeçalhos: Content-Encoding (como está comprimido) e Content-Type do conteúdo original, não do arquivo comprimido:
| Arquivo | Content-Encoding | Content-Type |
|---|---|---|
*.wasm.br / *.wasm.gz | br / gzip | application/wasm |
*.js.br / *.js.gz | br / gzip | application/javascript |
*.data.br / *.data.gz | br / gzip | application/octet-stream |
O tipo application/wasm importa por dois motivos: é ele que permite ao navegador compilar o WebAssembly enquanto baixa (estágio 3 do cap. 3), e a falta dele gera avisos ou perda dessa otimização. Se algo estiver errado, o loader da Unity costuma exibir uma mensagem como "não foi possível ler o arquivo… a compressão da build foi ativada, mas o servidor não entregou o cabeçalho Content-Encoding".
7.3 Exemplos de servidor
Nginx, servindo arquivos Brotli pré-comprimidos:
location ~ \.data\.br$ {
types {} default_type application/octet-stream;
add_header Content-Encoding br;
}
location ~ \.wasm\.br$ {
types {} default_type application/wasm;
add_header Content-Encoding br;
}
location ~ \.js\.br$ {
types {} default_type application/javascript;
add_header Content-Encoding br;
}
Apache (o servidor trata cada extensão separadamente: .br é a codificação, .wasm é o tipo):
<IfModule mod_mime.c>
AddEncoding br .br
AddEncoding gzip .gz
AddType application/wasm .wasm
AddType application/javascript .js
AddType application/octet-stream .data
</IfModule>
Para Vercel e Netlify, defina os cabeçalhos por padrão de caminho no arquivo de configuração da plataforma (headers no vercel.json; arquivo _headers no Netlify), seguindo a tabela 7.2. Em S3 + CloudFront, defina Content-Encoding e Content-Type como metadados de cada objeto ao enviar. Sempre teste:
curl -sI -H "Accept-Encoding: br, gzip" https://seu-site/Build/jogo.wasm.br | grep -i -E "content-(type|encoding)"
# esperado: content-encoding: br e content-type: application/wasm
7.4 Cache
- Arquivos da pasta Build com hash no nome:
Cache-Control: public, max-age=31536000, immutable. O hash muda a cada versão, então o cache de um ano é seguro. index.html: sem cache longo (no-cacheou poucos minutos), para que uma nova versão apareça imediatamente.- CDN: hospedar perto do aluno (no Brasil, uma CDN com pontos de presença no país) reduz a latência do estágio 1.
7.5 Onde hospedar: o que muda
| Destino | Controle de cabeçalhos | Estratégia |
|---|---|---|
| itch.io | Tratado pela plataforma para builds Unity comprimidas (teste a sua) | Envie o zip da build; ótimo para portfólio |
| GitHub Pages | Não dá para definir cabeçalhos por arquivo | Compressão Disabled ou Gzip com Decompression Fallback |
| Vercel / Netlify | Sim, por arquivo de configuração | Brotli ou Gzip pré-comprimidos + cabeçalhos da 7.2 |
| S3 + CloudFront | Sim, por metadados | Pré-comprimido com metadados, ou Disabled com compressão da CDN |
| Dentro do LMS (SCORM) | Normalmente não | Disabled ou Decompression Fallback (cap. 9) |
Saber diagnosticar "a build abre no itch.io mas não no LMS do cliente" pela aba Network (é quase sempre Content-Encoding ou Content-Type) é a habilidade de suporte que mais economiza horas em projetos de e-learning.
Faça a mesma build em Gzip, Brotli e Disabled, publique nas hospedagens que você tem e preencha: tamanho transferido, tempo até jogar, se abre. Qual combinação vence no seu público?
Remova o cabeçalho Content-Encoding do servidor de teste e recarregue. Anote a mensagem de erro do loader. Ligue o Decompression Fallback e recarregue: o que mudou no tamanho do .loader.js e no tempo de carga?
Ver o que esperar
Sem o cabeçalho, o jogo falha ao ler os arquivos comprimidos e mostra uma mensagem sobre a configuração do servidor. Com o fallback ligado, o jogo abre, o carregador fica maior (porque inclui o decompressor) e a carga demora mais que com o cabeçalho correto. É a prova de que o fallback é uma rede de segurança, e não uma solução.
Capítulo 08 Avançado
Carregar menos: Addressables, memória e tela de loading
Quando reduzir não basta, o jogo passa a carregar só o que a primeira jogada precisa e busca o resto enquanto o aluno joga.
8.1 Addressables: build pequena + conteúdo remoto
A ideia: a build Web inicial contém apenas um "bootstrap" (menu, tela de loading, sistema de dados) e o conteúdo de cada fase ou módulo vive em grupos Addressables hospedados numa CDN. O jogador baixa cada fase quando precisa dela, e o navegador guarda os pacotes em cache.
using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class CarregadorDeFase : MonoBehaviour
{
[SerializeField] Slider barra;
public IEnumerator Carregar(string chaveDaFase)
{
// 1) Quanto falta baixar? (0 se já está em cache)
var tamanho = Addressables.GetDownloadSizeAsync(chaveDaFase);
yield return tamanho;
if (tamanho.Result > 0)
{
var baixar = Addressables.DownloadDependenciesAsync(chaveDaFase);
while (!baixar.IsDone) { barra.value = baixar.PercentComplete; yield return null; }
Addressables.Release(baixar);
}
Addressables.Release(tamanho);
// 2) Entrar na fase (a cena é um endereço)
yield return Addressables.LoadSceneAsync(chaveDaFase, LoadSceneMode.Single);
}
}
Pontos de atenção específicos da web:
- CORS: se a CDN está em outro domínio, ela precisa enviar
Access-Control-Allow-Originpara o domínio do jogo, senão o navegador bloqueia os pacotes. - Compressão dos bundles: prefira LZ4 ou sem compressão do bundle, deixando a compressão de transporte para o servidor; LZMA é desaconselhada na web. Confirme na documentação da sua versão.
- Agrupe por uso: assets usados juntos ficam no mesmo grupo (uma fase, um módulo), para não baixar 30 arquivinhos.
- Falha de rede: trate o erro e ofereça "Tentar de novo". Redes escolares caem.
Para outras cenas grandes que ficam na própria build, use SceneManager.LoadSceneAsync: o progresso reportado para em 0,9 até você liberar allowSceneActivation, o que permite terminar a tela de loading antes de trocar.
8.2 Tela de loading no template
O carregador da Unity só cria o jogo depois que createUnityInstance termina, e aceita um callback de progresso de 0 a 1. Personalize um WebGL Template (pasta Assets/WebGLTemplates/SuaMarca, escolhida em Player Settings → Resolution and Presentation) para mostrar marca, barra e mensagem em HTML:
<canvas id="unity-canvas" tabindex="-1"></canvas>
<div id="loading">
<div id="bar"><div id="fill"></div></div>
<p id="msg">Carregando o jogo…</p>
</div>
<script>
var s = document.createElement("script");
s.src = "Build/{{{ LOADER_FILENAME }}}";
s.onload = function () {
createUnityInstance(document.querySelector("#unity-canvas"), {
dataUrl: "Build/{{{ DATA_FILENAME }}}",
frameworkUrl: "Build/{{{ FRAMEWORK_FILENAME }}}",
codeUrl: "Build/{{{ CODE_FILENAME }}}",
streamingAssetsUrl: "StreamingAssets",
companyName: "{{{ COMPANY_NAME }}}",
productName: "{{{ PRODUCT_NAME }}}",
productVersion: "{{{ PRODUCT_VERSION }}}",
devicePixelRatio: 1 // menos pixels em telas de alta densidade
}, function (p) {
document.getElementById("fill").style.width = (p * 100) + "%";
}).then(function (u) {
window.unityInstance = u;
document.getElementById("loading").hidden = true;
}).catch(function (e) {
document.getElementById("msg").textContent =
"Não foi possível carregar o jogo. Recarregue a página.";
console.error(e);
});
};
document.body.appendChild(s);
</script>
As variáveis {{{ … }}} são preenchidas pela Unity na hora do build, com os nomes reais dos arquivos. Repare no .catch: uma mensagem humana vale mais que uma tela em branco. E o devicePixelRatio: 1 reduz a resolução interna em telas de alta densidade, o que ajuda bastante em celulares.
8.3 Percepção de espera
- Barra honesta: não invente progresso. Use o valor real do callback.
- Diga o que está acontecendo e dê algo para ler: dica de jogo, objetivo da fase, aviso de que "na primeira vez demora mais; depois é rápido".
- Termine com um gesto: "Toque para começar" libera áudio e tela cheia (cap. 2) e evita que o aluno perca a introdução.
- Pré-aqueça: compile shaders e carregue a primeira fase durante o loading (cap. 6).
8.4 Data caching
Em Player Settings → Publishing Settings, a opção Data Caching guarda o .data no IndexedDB do navegador, evitando baixá-lo de novo em visitas seguintes (útil em turmas que jogam toda semana). Três ressalvas: ele não evita o desempacotar (estágio 4); a cada versão nova, o cache precisa ser reconstruído; e o navegador pode limitar ou negar armazenamento (janela privada, cotas, iframes de terceiros em alguns navegadores). Sempre teste em janela anônima e não dependa dele para a jogabilidade.
8.5 Memória: o limite silencioso
A memória da aba tem três áreas: o heap da Unity (objetos, cenas, shaders), os dados (o .data desempacotado) e o comportamento do coletor de lixo. Nas configurações da build Web, o Initial Memory Size (tamanho inicial do heap) e o Memory Growth Mode controlam como a memória cresce. Recomendações:
- Em celulares, ajuste o tamanho inicial ao uso típico do jogo; crescer o heap durante a partida custa tempo e pode falhar.
- Como o GC só roda entre frames, alocações repetidas em um mesmo laço acumulam sem serem coletadas: use
StringBuilder, pré-aloque listas e prefiraNativeArraypara dados temporários. - Use uma build de desenvolvimento com Profiler para medir o heap; desligue-a na entrega.
8.6 Celulares e tablets
As versões recentes da Unity suportam navegadores móveis (Chrome no Android, Safari no iOS), mas com limites de memória menores e comportamentos próprios: teclado virtual em campos de texto do jogo historicamente exige soluções em HTML/.jslib; a tela cheia no iPhone tem restrições; e a orientação da tela precisa ser tratada. Se o seu público usa celular, inclua-o na matriz de testes desde o começo (cap. 12).
"Carregamento em duas fases com Addressables e conteúdo remoto" é um requisito recorrente em jogos de treinamento com muitos módulos. Saber montar isso e hospedar numa CDN com CORS correto é o que permite entregar um curso de 20 fases sem que a primeira abertura leve minutos.
Para um jogo de 6 fases de ~5 MB cada e um menu de ~4 MB, desenhe: o que vai na build inicial, o que vai em Addressables, quando cada grupo é baixado e o que o aluno vê em cada momento. Compare a espera da primeira jogada com a de uma build única.
Ver exemplo resolvido
Build única: ~34 MB antes de qualquer jogada (~54 s a 5 Mbps). Com Addressables: build inicial ~4 MB + fase 1 (~5 MB) = ~9 MB antes da primeira jogada (~14 s a 5 Mbps); as fases 2 a 6 são baixadas durante a fase anterior ou na tela de seleção. O custo: mais complexidade e o tratamento de falhas de rede.
Crie um WebGL Template com a sua marca, barra de progresso real e mensagem de erro amigável. Teste-o com a rede lenta do DevTools e com o servidor "quebrado" do exercício 7.2.
Capítulo 09 Avançado
SCORM na prática: empacotar e reportar ao LMS
SCORM é antigo, limitado e onipresente. Se o seu cliente tem um LMS, é provável que a pergunta dele seja "vocês exportam SCORM?".
9.1 O que é o SCORM
SCORM (Sharable Content Object Reference Model) é um conjunto de especificações da ADL que faz duas coisas: define como empacotar um conteúdo (um zip com um arquivo imsmanifest.xml) e como esse conteúdo conversa com o LMS (uma API em JavaScript que o LMS expõe). As versões que você encontra são a 1.2 (a mais compatível) e a 2004 (várias edições; com sequenciamento mais rico e mais complexidade). O que o SCORM registra é pouco: status de conclusão, nota, tempo e um pouco de estado salvo. Para dados mais ricos, veja o cap. 10.
9.2 Como a conversa acontece
O LMS abre o seu conteúdo (o index.html da build) dentro de uma janela ou iframe e coloca nessa hierarquia um objeto: window.API (SCORM 1.2) ou window.API_1484_11 (SCORM 2004). O seu código procura esse objeto subindo pela cadeia de window.parent e depois chama métodos:
| Ideia | SCORM 1.2 | SCORM 2004 |
|---|---|---|
| Objeto da API | API | API_1484_11 |
| Iniciar / encerrar | LMSInitialize("") / LMSFinish("") | Initialize("") / Terminate("") |
| Gravar / ler | LMSSetValue(k, v) / LMSGetValue(k) | SetValue(k, v) / GetValue(k) |
| Salvar | LMSCommit("") | Commit("") |
| Conclusão / aprovação | cmi.core.lesson_status (passed, failed, completed, incomplete, browsed, not attempted) | cmi.completion_status + cmi.success_status (separados) |
| Nota | cmi.core.score.raw (0 a 100) | cmi.score.raw e cmi.score.scaled (−1 a 1) |
| Tempo da sessão | cmi.core.session_time (formato HH:MM:SS) | cmi.session_time (duração ISO 8601, ex. PT4M12S) |
| Estado salvo | cmi.suspend_data (até 4096 caracteres) | cmi.suspend_data (até 64000 caracteres) |
9.3 A ponte JavaScript ↔ C#
O jogo em C# não enxerga window.API. A ponte tem três peças: um bridge em JavaScript no template (acha a API e a envolve), um plugin .jslib (a ponte entre o wasm e o JavaScript) e uma classe C# que o resto do jogo chama. Este exemplo é SCORM 1.2.
1) No index.html do template:
<script>
window.scormBridge = (function () {
function achar(win) {
for (var n = 0; n < 10; n++) {
try { if (win.API) return win.API; } catch (e) { return null; } // outro domínio: bloqueado
if (!win.parent || win.parent === win) return null;
win = win.parent;
}
return null;
}
var api = achar(window) || (window.opener ? achar(window.opener) : null);
var ok = !!api && String(api.LMSInitialize("")) === "true";
return {
ativo: function () { return ok; },
setValue: function (k, v){ return ok && String(api.LMSSetValue(k, v)) === "true"; },
getValue: function (k) { return ok ? String(api.LMSGetValue(k)) : ""; },
commit: function () { return ok && String(api.LMSCommit("")) === "true"; },
finish: function () { if (ok) { api.LMSCommit(""); api.LMSFinish(""); ok = false; } }
};
})();
window.addEventListener("beforeunload", function () { window.scormBridge.finish(); });
</script>
2) Assets/Plugins/WebGL/ScormBridge.jslib:
mergeInto(LibraryManager.library, {
ScormSetValue: function (keyPtr, valuePtr) {
var k = UTF8ToString(keyPtr), v = UTF8ToString(valuePtr);
return (window.scormBridge && window.scormBridge.setValue(k, v)) ? 1 : 0;
},
ScormCommit: function () {
if (window.scormBridge) window.scormBridge.commit();
},
ScormAtivo: function () {
return (window.scormBridge && window.scormBridge.ativo()) ? 1 : 0;
}
});
3) Scorm12.cs:
using System.Runtime.InteropServices;
using UnityEngine;
public static class Scorm12
{
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")] static extern int ScormSetValue(string key, string value);
[DllImport("__Internal")] static extern void ScormCommit();
[DllImport("__Internal")] static extern int ScormAtivo();
#else
// No Editor e fora do LMS, apenas registra no Console.
static int ScormSetValue(string k, string v) { Debug.Log($"[SCORM] {k} = {v}"); return 1; }
static void ScormCommit() { }
static int ScormAtivo() { return 0; }
#endif
public static bool Ativo => ScormAtivo() == 1;
public static void ReportarResultado(int notaPercentual, bool aprovado, int segundos)
{
ScormSetValue("cmi.core.score.raw", Mathf.Clamp(notaPercentual, 0, 100).ToString());
ScormSetValue("cmi.core.lesson_status", aprovado ? "passed" : "failed");
ScormSetValue("cmi.core.session_time", $"{segundos / 3600:0000}:{segundos % 3600 / 60:00}:{segundos % 60:00}");
ScormCommit();
}
}
Três cuidados: chame LMSCommit em marcos (fim de fase), não a cada frame; guarde suspend_data num formato compacto (o limite do 1.2 é de 4096 caracteres); e, se o manifesto define uma nota mínima (adlcp:masteryscore), muitos LMS decidem aprovado ou reprovado a partir da nota, ignorando o status que você enviou.
9.4 O manifesto e o pacote
O pacote é um zip com imsmanifest.xml na raiz, ao lado de index.html, Build/, TemplateData/ etc. Um manifesto mínimo do SCORM 1.2:
<?xml version="1.0" encoding="UTF-8"?>
<manifest identifier="br.com.exemplo.jogo-fracoes" version="1"
xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_rootv1p2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<metadata>
<schema>ADL SCORM</schema>
<schemaversion>1.2</schemaversion>
</metadata>
<organizations default="org1">
<organization identifier="org1">
<title>Jogo das Frações</title>
<item identifier="item1" identifierref="res1">
<title>Jogo das Frações</title>
</item>
</organization>
</organizations>
<resources>
<resource identifier="res1" type="webcontent" adlcp:scormtype="sco" href="index.html">
<file href="index.html"/>
<!-- liste todos os arquivos do pacote (ou gere a lista com uma ferramenta) -->
</resource>
</resources>
</manifest>
Repare no adlcp:scormtype em minúsculas: é o nome no SCORM 1.2; no 2004 é adlcp:scormType. Erros de manifesto são a segunda causa de "o LMS não aceita o pacote". Teste sempre no SCORM Cloud (serviço da Rustici para validar pacotes) e em um Moodle local antes de entregar.
9.5 Os quatro problemas que só aparecem no LMS
Compressão sem cabeçalho
O LMS serve os arquivos do pacote com os seus próprios cabeçalhos, sem Content-Encoding para .br ou .gz. Solução: gere a build com compressão Disabled (ou use Decompression Fallback) para o pacote SCORM.
Limite de upload
Muitos LMS limitam o tamanho do pacote (é configurável, mas o administrador precisa alterar). Uma build de 100 MB descomprimida bate nesse teto. Solução: reduzir a build (caps. 5 e 6) ou usar um pacote-casca (9.6).
Outro domínio, sem API
Se o jogo está hospedado fora do LMS (em outra origem), o navegador impede o acesso a window.parent.API. O bridge não acha a API e nada é registrado. Solução: 9.6 ou cmi5 (cap. 10).
Armazenamento em iframe
Dentro de um iframe de outro domínio, alguns navegadores particionam ou bloqueiam o IndexedDB, onde PlayerPrefs e o data caching guardam dados. Não use PlayerPrefs como única cópia do progresso: use suspend_data ou um servidor.
9.6 O pacote-casca (hospedar fora, reportar dentro)
Quando a build é grande demais para o LMS ou precisa de cabeçalhos que ele não permite, o pacote SCORM contém só uma página-casca pequena. Ela encontra a API do SCORM (está na mesma origem do LMS) e embute o jogo hospedado na sua CDN num iframe, trocando mensagens com ele via postMessage: o jogo envia "fim da fase, nota 80" e a casca traduz para LMSSetValue. É o mesmo desenho do bridge do 9.3, com uma camada de mensagens no meio. Validar a origem da mensagem (event.origin) é obrigatório, senão qualquer página poderia registrar notas.
// Na página-casca (dentro do pacote SCORM), escutando o jogo hospedado fora:
var ORIGEM_DO_JOGO = "https://jogos.exemplo.com.br";
window.addEventListener("message", function (ev) {
if (ev.origin !== ORIGEM_DO_JOGO) return; // ignora qualquer outra origem
var m = ev.data || {};
if (m.tipo === "resultado") {
scormBridge.setValue("cmi.core.score.raw", String(m.nota));
scormBridge.setValue("cmi.core.lesson_status", m.aprovado ? "passed" : "failed");
scormBridge.commit();
}
});
9.7 Limites do SCORM (e por que o cap. 10 existe)
- Só existe dentro do LMS, aberto pelo LMS. Sem app nativo, sem jogo fora dele.
- Modelo de dados pobre: uma nota e um status. Você não descobre em qual pergunta o aluno errou, quanto tempo passou em cada fase ou que escolha fez.
- Depende da janela do navegador aberta: perder a conexão no fim da sessão pode perder o registro.
Editais e contratos corporativos costumam listar "exportação SCORM 1.2 ou 2004" como requisito. Ter um pacote-modelo já testado no SCORM Cloud e em Moodle (com o bridge, o manifesto e o processo de build) vira ativo reutilizável de portfólio e de proposta comercial. Para o lado pedagógico e de plataformas, veja Técnicas Modernas de Ensino e Aprendizagem.
Pegue a build do exercício 2.1 (compressão Disabled), acrescente o bridge, a classe Scorm12 e um botão "Concluir" que envia nota 100 e passed. Escreva o manifesto, gere o zip e suba no SCORM Cloud. Confira se a nota e o status chegaram.
Ver checklist de erros comuns
1) imsmanifest.xml dentro de uma subpasta em vez de na raiz do zip. 2) O href do recurso não bate com o nome real do index.html. 3) Compressão da build ligada e sem cabeçalhos. 4) Esqueceu de chamar LMSCommit. 5) Testou só no Editor, onde o bridge não existe.
Usando a tabela 9.2, escreva o que muda no bridge, no .jslib e na classe C# para suportar SCORM 2004 (nome da API, métodos, chaves, formato de tempo e de nota).
Capítulo 10 Avançado · Interativo
xAPI, LRS e cmi5: dados além do LMS
Se o SCORM diz "passou ou não passou", o xAPI conta a história: quem fez o quê, em qual contexto, com qual resultado.
10.1 O modelo em uma frase
O xAPI (Experience API, também chamado Tin Can) registra experiências de aprendizagem como statements, frases estruturadas em JSON no formato ator + verbo + objeto: "Ana respondeu à pergunta 4 do nível 2". Os statements vão por HTTP para um LRS (Learning Record Store), que os guarda e permite consultar. Ao contrário do SCORM, o jogo não precisa estar dentro do LMS: pode estar em qualquer lugar da web. A versão 1.0.3 é a mais implantada; o xAPI 2.0 foi padronizado pelo IEEE em 2023.
10.2 Anatomia de um statement
{
"actor": {
"objectType": "Agent",
"account": { "homePage": "https://jogos.exemplo.com.br", "name": "aluno-4f2a91" }
},
"verb": {
"id": "http://adlnet.gov/expapi/verbs/answered",
"display": { "pt-BR": "respondeu" }
},
"object": {
"objectType": "Activity",
"id": "https://jogos.exemplo.com.br/fracoes/nivel-2/pergunta-4",
"definition": { "name": { "pt-BR": "Fração equivalente a 3/4" }, "type": "http://adlnet.gov/expapi/activities/cmi.interaction" }
},
"result": {
"success": false,
"response": "6/9",
"duration": "PT23S"
},
"context": {
"registration": "0b3f5ec3-2d09-4b9e-9f4a-6a1a8f5e7a10",
"contextActivities": { "parent": [ { "id": "https://jogos.exemplo.com.br/fracoes/nivel-2" } ] }
},
"timestamp": "2026-09-19T14:03:22.000Z"
}
| Campo | Para que serve | Cuidado |
|---|---|---|
actor | Quem fez. Identificado por conta (account) ou e-mail (mbox). | Prefira um identificador pseudônimo (cap. 11), não o e-mail. |
verb | O que fez. Tem um IRI (um identificador em formato de URL) e um texto para exibição. | Reaproveite verbos do vocabulário ADL (completed, passed, failed, attempted, answered, experienced, progressed...) antes de inventar. |
object | Sobre o que. Uma atividade com IRI estável e nome. | O IRI deve ser estável: se mudar a cada versão, você não consegue comparar resultados. |
result | Nota (score.scaled de 0 a 1, raw, min, max), success, completion, response, duration. | A duração é ISO 8601 (PT23S, PT4M12S). |
context | Em que contexto: sessão (registration), atividade-pai, extensões próprias. | É aqui que você liga a pergunta à fase e ao curso. |
10.3 Demo: monte um statement
Altere os campos e veja o JSON mudar. Repare que o ator usa uma conta pseudônima e que a duração é convertida para o formato ISO 8601.
10.4 O LRS
Um LRS expõe um endpoint /statements: você faz POST com o JSON, envia o cabeçalho X-Experience-API-Version (por exemplo, 1.0.3) e credenciais. Há LRS de código aberto que você hospeda (como o Learning Locker, o Yet Analytics SQL LRS e o Trax LRS), LRS comerciais com plano gratuito para testes e o LRS embutido do SCORM Cloud. Confirme os planos e a manutenção atuais antes de escolher.
10.5 Enviando statements do Unity
Na build Web, use UnityWebRequest (nada de HttpClient ou sockets). Bibliotecas de xAPI para .NET existem, mas confirme a manutenção e a compatibilidade com IL2CPP e WebGL antes de adotar; para um envio simples, cerca de trinta linhas resolvem e você controla tudo:
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
public class XapiSender : MonoBehaviour
{
[SerializeField] string endpoint = "https://lrs.exemplo.com.br/xapi/statements";
string authorization; // recebido em tempo de execução; nunca escrito no projeto
public void Configurar(string valorAuthorization) => authorization = valorAuthorization;
public IEnumerator Enviar(string statementJson)
{
using var req = new UnityWebRequest(endpoint, UnityWebRequest.kHttpVerbPOST);
req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(statementJson));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
req.SetRequestHeader("X-Experience-API-Version", "1.0.3");
req.SetRequestHeader("Authorization", authorization);
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
Debug.LogWarning($"xAPI falhou ({req.responseCode}): {req.error}"); // enfileirar para tentar de novo
}
}
Tudo o que vai no .data ou no .wasm pode ser extraído por qualquer pessoa com o DevTools. Se a chave do LRS está no projeto, qualquer aluno consegue gravar (ou apagar) statements de qualquer outro. A saída correta é uma credencial de curta duração entregue em tempo de execução: pelo cmi5 (10.7) ou por um pequeno servidor seu que valida a sessão e repassa ao LRS.
10.6 Robustez: rede, CORS e fim de sessão
- CORS: o LRS precisa aceitar requisições do domínio do jogo, inclusive a requisição de verificação (
OPTIONS) e os cabeçalhosAuthorization,Content-TypeeX-Experience-API-Version. - Fila com reenvio: guarde os statements pendentes numa lista e tente de novo com espera crescente. Redes de escola caem.
- Envie em lote: o endpoint aceita um array de statements; agrupe eventos de baixa importância em vez de enviar um por clique.
- Fim de sessão: ao fechar a aba, use
fetch(url, { keepalive: true, headers: … })para o statement final. Onavigator.sendBeaconnão permite definir os cabeçalhos que o xAPI exige. - Idempotência: gere um UUID por statement (
id) para que um reenvio não duplique o evento.
10.7 cmi5: o xAPI lançado por um LMS
O cmi5 é um perfil do xAPI para conteúdo lançado por um LMS: ele resolve o que o SCORM não resolve, porque não depende da janela pai, então o jogo pode estar hospedado em qualquer domínio. O LMS abre a URL do jogo com parâmetros de lançamento:
endpoint: o LRS que receberá os statements;fetch: uma URL de uso único que devolve o token de autorização da sessão (a credencial de curta duração da caixa acima);actor: quem está jogando;registration: o identificador da matrícula, que deve constar em todo statement;activityId: o IRI da atividade.
O perfil também define um ciclo de vida: initialized deve ser o primeiro statement da sessão e terminated o último; no meio ficam completed, passed, failed, abandoned, waived e quantos statements xAPI próprios você quiser. O suporte a cmi5 varia entre LMS: confira o do seu cliente antes de prometê-lo.
10.8 Comparando os três caminhos
| SCORM | xAPI "solto" | cmi5 | |
|---|---|---|---|
| Quem lança | LMS | Qualquer um (link, app, LMS) | LMS |
| Onde o jogo pode estar | No LMS (ou pacote-casca) | Qualquer domínio | Qualquer domínio |
| Dados | Nota, status, tempo, estado | Qualquer evento | Qualquer evento + ciclo de vida definido |
| Autorização | Sessão do LMS | Você resolve (risco de chave exposta) | Token de curta duração vindo do LMS |
| Compatibilidade de LMS | Quase universal | Depende de LRS | Varia (cresceu, mas não é universal) |
| Quando escolher | O cliente exige "SCORM" e ponto | Análise rica, fora de LMS (portfólio, pesquisa) | LMS moderno + jogo hospedado fora |
"Trabalho com SCORM e xAPI" é diferencial em vagas de EdTech, treinamento corporativo e learning analytics. Um portfólio com um jogo publicado, um dashboard simples lendo o LRS e um texto explicando o que se decidiu com os dados mostra o ciclo completo, e não só a parte de programação.
Para o jogo da ficha do exercício 1.2, escreva a lista de statements que ele enviará: para cada um, o verbo (do vocabulário ADL, quando possível), o IRI da atividade e o que vai em result e context. Limite-se a 8 tipos de evento.
Ver exemplo resolvido
(1) initialized no início da sessão; (2) attempted ao abrir cada fase; (3) answered a cada pergunta, com response, success e duration; (4) progressed ao concluir 50% de uma fase (com progress em extensão); (5) completed ao fim de cada fase, com score.scaled; (6) passed ou failed no fim do jogo; (7) terminated ao sair; (8) um evento próprio (extensão) para "pediu dica". Todos com registration e a fase como atividade-pai.
Suba um LRS de teste (o do SCORM Cloud ou um de código aberto local), envie três statements a partir da sua build Web usando a classe XapiSender e confirme-os na interface do LRS. Depois, remova o CORS do LRS e observe o erro no Console.
Capítulo 11 Muito avançado
Desenho da evidência, privacidade e acessibilidade
Enviar dados é fácil; enviar dados que respondam a uma pergunta pedagógica é o trabalho de verdade. E nada disso vale se o dado for ilegal ou se o aluno não consegue jogar.
11.1 Comece pela decisão, não pelos eventos
O erro típico é registrar tudo "porque pode ser útil". Você acaba com milhões de linhas e nenhuma resposta. Faça o caminho inverso, inspirado no Evidence-Centered Design (Mislevy, Almond e Lukas, 2003):
Modelo do aluno
O que você quer saber? Ex.: "domina frações equivalentes?", "sabe seguir o protocolo de evacuação?".
Modelo de tarefa
Que situações do jogo obrigam o aluno a mostrar esse conhecimento? Ex.: um puzzle em que só a fração equivalente abre a porta.
Modelo de evidência
Que comportamentos observáveis contam como sinal, e como interpretá-los? Ex.: acertos, tipo de erro, tentativas, uso de dica.
Instrumentação
Só agora: quais statements o jogo envia para registrar essas evidências.
Essa cadeia liga cada evento a uma pergunta. É também a base da avaliação embutida (stealth assessment), defendida por Valerie Shute: a avaliação acontece dentro do jogo, sem interromper a experiência com um teste.
11.2 Taxonomia de eventos de um serious game
| Categoria | Exemplos | Pergunta que responde |
|---|---|---|
| Ciclo de vida | iniciou, terminou, abandonou, retomou | Quantos chegam ao fim? Onde desistem? |
| Progresso | abriu fase, concluiu fase, tempo por fase | Onde o jogo trava a turma? |
| Desempenho | respondeu, acertou/errou, tentativa nº | Quais conceitos ele domina? |
| Erro qualificado | escolheu o distrator B, erro de sinal | Qual é a concepção equivocada por trás do erro? |
| Apoio | pediu dica, abriu glossário | De que ajuda precisa? |
| Decisão | escolheu a opção X num dilema | Como pensa em situações abertas? (simulações, soft skills) |
- Agregue no cliente o que puder (um resumo por fase em vez de um evento por clique).
- Diferencie erro de ruído: um clique sem intenção não é evidência. Registre o contexto (tempo de reação, tentativas) para poder filtrar depois.
- Cuidado com métricas de fachada: tempo de tela não é aprendizagem. Quando a métrica vira meta, o aluno passa a jogar para a métrica.
11.3 Perfis e padrões
Para não reinventar a semântica, procure perfis xAPI existentes. O xAPI Serious Games Profile (xAPI-SG), produzido por pesquisadores da Universidad Complutense de Madrid, define verbos, atividades e extensões para eventos de jogos educativos (iniciar e completar níveis, escolhas, uso de itens). Vale conhecê-lo como referência de vocabulário; confirme a versão e a manutenção antes de basear o projeto nele. Um vocabulário compartilhado é o que permite comparar resultados entre jogos e turmas.
11.4 Privacidade: LGPD e minimização
Statements descrevem pessoas, muitas vezes crianças. Trate-os como dados pessoais:
- Minimize: colete só o que responde à decisão do 11.1. Se você não vai usar, não colete.
- Pseudonimize: use um identificador que só o cliente consegue ligar à pessoa (o
account.namedo exemplo), nunca e-mail, CPF ou nome real dentro do statement. - Crianças e adolescentes: a LGPD (art. 14) tem regras específicas para o tratamento de dados desse público, com o consentimento de responsável em muitos casos. Consulte o jurídico do cliente; este texto não é aconselhamento jurídico.
- Retenção e acesso: defina por quanto tempo os dados ficam guardados, quem os vê e como um aluno pede a exclusão.
- Transparência: diga ao aluno e ao responsável, em linguagem simples, o que o jogo registra e para quê.
11.5 Acessibilidade em jogos web
Um jogo em canvas é opaco para leitores de tela: o navegador enxerga um retângulo de pixels. Se o serious game é para um público amplo (e em contexto educacional ou corporativo, frequentemente há obrigação de acessibilidade), planeje desde o início:
- Teclado: tudo o que se faz com o mouse deve ser possível pelo teclado.
- Contraste e tamanho: texto legível, sem depender só de cor para informar.
- Legendas e alternativas sonoras para narração e efeitos importantes.
- Sem limite de tempo rígido ou com opção de ampliá-lo.
- Alternativa em HTML: para conteúdo essencial, considere oferecer a atividade também fora do canvas, ou expor texto real na página ao redor.
Learning engineers e designers instrucionais valorizam quem consegue traduzir "queremos saber se aprenderam" em um plano de evidência antes de escrever uma linha de código. É a diferença entre entregar um jogo e entregar uma solução de aprendizagem auditável. Para o lado de ciência de dados desse trabalho, veja também Datavis com Python e Webdesign.
Para um objetivo de aprendizagem do seu jogo, preencha as quatro caixas de 11.1 e derive dois statements que respondam à pergunta. Descarte um evento que você ia registrar e que não responde a nenhuma pergunta.
Ver exemplo resolvido
Modelo do aluno: reconhece frações equivalentes. Tarefa: escolher, entre quatro portas, a que mostra fração equivalente a 3/4. Evidência: resposta correta na 1ª tentativa; tipo de erro (compara só numeradores? só denominadores?). Statements: answered com response, success e uma extensão tipo-de-erro; progressed ao fechar o conjunto de portas. Descartado: "moveu o mouse" (não responde a nada).
Liste todo dado pessoal que o seu jogo coleta, ou coletaria, e classifique cada item como necessário ou dispensável. Para os necessários, defina o pseudônimo, o prazo de retenção e quem pode ver.
Jogue a primeira fase só com o teclado, depois em 200% de zoom e depois com o som desligado. Anote onde o jogo deixa de ser jogável ou compreensível.
Capítulo 12 Prática · Plano
Pipeline, cenários de decisão e plano de 30 dias
Tudo o que vimos vira rotina quando a build, o teste e a verificação de tamanho deixam de ser manuais.
12.1 Automatizando o build
Faça a build Web em integração contínua para que qualquer commit gere um pacote testável. O projeto de código aberto GameCI oferece ações do GitHub para compilar projetos Unity (é preciso configurar a licença); o resultado é uma pasta Build pronta para publicar. Depois da build, verifique o orçamento do cap. 4 automaticamente:
#!/usr/bin/env bash
# Falha o pipeline se o total transferido passar do orçamento (em MB)
ORCAMENTO_MB=12
TOTAL_KB=$(du -sk Builds/Web/Build | cut -f1)
LIMITE_KB=$((ORCAMENTO_MB * 1024))
echo "Build: $((TOTAL_KB / 1024)) MB (limite: ${ORCAMENTO_MB} MB)"
if [ "$TOTAL_KB" -gt "$LIMITE_KB" ]; then
echo "ERRO: a build estourou o orçamento de tamanho"; exit 1
fi
Faça o mesmo com o servidor: um passo de curl -I que confere Content-Encoding e Content-Type depois de cada deploy (7.3). Para o lado de infraestrutura, veja DevOps, Docker e Kubernetes.
12.2 Matriz de testes
| Eixo | Itens mínimos |
|---|---|
| Navegadores | Chrome, Firefox, Safari (macOS e iOS), Edge; Chrome no Android |
| Rede | Perfil lento do DevTools; cache limpo e cache preenchido; janela anônima |
| Dispositivo | Um PC fraco, um Chromebook, um celular mediano |
| Contexto | Direto no link; dentro do iframe do LMS; SCORM Cloud e Moodle |
| Servidor | Content-Encoding, Content-Type, cache, CORS do LRS e da CDN |
| Dados | Statements chegam ao LRS; nota e status chegam ao LMS; fim de sessão registra |
| Acessibilidade | Teclado, zoom, sem som |
12.3 Cenários de decisão (ilustrativos)
Os cenários abaixo são hipotéticos, construídos para treinar a decisão; não descrevem projetos reais.
Cenário A — A escola com Wi-Fi compartilhado
Educação básicaUma rede municipal quer um jogo de matemática em Chromebooks, entregue pelo Moodle. Cada turma tem 30 alunos e um link compartilhado de 60 Mbps. A build atual tem 28 MB e o Moodle limita uploads a 50 MB.
Decisão: orçamento de ~8 MB (cap. 4); build inicial mínima + Addressables (cap. 8); pacote-casca SCORM com o jogo na CDN (9.6); compressão Brotli na CDN, em HTTPS, ou Disabled dentro do pacote.
Cenário B — Treinamento de segurança corporativo
CorporativoUma empresa quer um simulador de identificação de riscos numa fábrica virtual. O LMS é moderno e o RH quer saber quais riscos os funcionários mais deixam passar, além de "concluiu ou não".
Decisão: cmi5 + xAPI (cap. 10), com evento por risco identificado ou ignorado, incluindo o tipo de risco; nota e conclusão também reportadas. Jogo hospedado fora do LMS, com token de curta duração.
Cenário C — Portfólio de desenvolvedor
CarreiraUma pessoa quer um serious game jogável no portfólio, sem LMS. O recrutador deve conseguir jogar em segundos pelo celular.
Decisão: build enxuta (≤ 10 MB), hospedagem em itch.io ou Vercel, compressão testada, uma tela "Toque para começar", e um LRS de teste com dashboard simples mostrando os dados de uso.
Cenário D — O cliente exige SCORM e o jogo tem 90 MB
Conflito de requisitosO LMS aceita só pacotes SCORM de até 100 MB e a pessoa administradora não pode alterar o limite. Os 90 MB descomprimidos estouram o teto depois de adicionar os arquivos auxiliares, e o LMS não serve arquivos comprimidos.
Decisão: pacote-casca (9.6) apontando para a CDN, com CORS e validação de origem; ou reduzir a build e mover o conteúdo pesado para Addressables. Documentar a decisão e o risco da dependência externa.
12.4 Checklist de entrega
- Build de release (Development Build desligado)?
- Tamanho transferido dentro do orçamento escrito?
- Managed Stripping, Strip Engine Code e módulos não usados revisados, e o jogo testado inteiro depois disso?
- Compressão compatível com o destino (servidor, CDN ou LMS)?
Content-EncodingeContent-Typeconferidos comcurl -I?- Nomes com hash e cache configurados;
index.htmlsem cache longo? - Tela de loading com progresso real, mensagem de erro e "Toque para começar"?
- Testado em celular, em rede lenta e em janela anônima?
- Pacote SCORM validado no SCORM Cloud e no LMS do cliente?
- Nenhuma credencial embutida na build?
- Eventos que respondem a perguntas, dados pseudonimizados e política de privacidade explicada?
- Acessibilidade mínima (teclado, contraste, legendas) verificada?
12.5 Plano de 30 dias (~45 min/dia)
| Semana | Foco | Entregável |
|---|---|---|
| 1 — Base e medição | Caps. 1 a 4: build vazia, medição, orçamento, diário de builds. | Diário de builds com o baseline e o orçamento escrito |
| 2 — Enxugar | Caps. 5 a 7: código, assets, compressão e servidor; teste com curl -I. | Build do seu jogo dentro do orçamento, publicada e verificada |
| 3 — Carregar e empacotar | Cap. 8 e 9: template com loading, Addressables (se precisar), pacote SCORM validado. | Pacote SCORM funcionando no SCORM Cloud |
| 4 — Dados e entrega | Caps. 10 e 11: plano de evidência, statements para um LRS, privacidade, acessibilidade. | Jogo publicado + LRS com dados reais + checklist 12.4 preenchido |
12.6 Bibliografia e fontes para seguir
- Serious games: Clark Abt, Serious Games (1970) · David Michael e Sande Chen, Serious Games: Games That Educate, Train, and Inform (2005) · James Paul Gee, What Video Games Have to Teach Us About Learning and Literacy (2003)
- Avaliação e dados: Valerie Shute, "Stealth assessment in computer-based games to support learning" (2011) · Mislevy, Almond e Lukas, "A Brief Introduction to Evidence-Centered Design" (ETS, 2003) · Serrano-Laguna e colegas, "Applying standards to systematize learning analytics in serious games" (2017)
- Especificações: ADL, xAPI Specification (repositório adlnet/xAPI-Spec) · cmi5 Specification (repositório AICC/CMI-5_Spec_Current) · documentação do SCORM 1.2 e 2004 da ADL; guias e explicações em scorm.com e xapi.com
- Unity: Manual da Unity, seção Web (build, compressão, memória, otimização de tamanho, Addressables) · Unity · Unity Essencial · C#
- Na trilha do site: Game Design · Gamification · Técnicas Modernas de Ensino e Aprendizagem · Spatial Computing, XR & WebXR
Nomes de menus, valores padrão e suporte a recursos (memória, mobile, WebAssembly, perfis de compressão) mudam entre versões da Unity, e também os planos e a manutenção de LRS e bibliotecas. Este guia dá o mapa e as razões; confirme cada detalhe na documentação da versão que você usa e meça na sua build.
Escreva uma página para o cliente (real ou fictício) com: o orçamento de tamanho e o resultado medido, a estratégia de compressão e hospedagem, o método de empacotamento escolhido e por quê, os eventos registrados e a decisão pedagógica que eles apoiam, e os riscos conhecidos. Esse texto é o seu melhor item de portfólio.