<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title><![CDATA[Cinevva Tutoriais]]></title>
        <link>https://app.cinevva.com</link>
        <description><![CDATA[Tutoriais práticos para quem cria jogos para a web.]]></description>
        <lastBuildDate>Mon, 12 Oct 2026 14:17:44 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>scripts/build-rss.mjs</generator>
        <copyright>Copyright 2024-present Cinevva</copyright>
        <item>
            <title><![CDATA[Ferramentas agênticas de IA para código]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/agentic-code-tools</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/agentic-code-tools</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Domine Claude Code, Cursor e GitHub Copilot: entenda fluxos de trabalho agênticos, boas práticas e quando usar cada ferramenta para maximizar a produtividade.]]></description>
            <content:encoded><![CDATA[<h1>Ferramentas agênticas de IA para código</h1>
<p>Os assistentes de programação com IA evoluíram do simples preenchimento automático para agentes autônomos capazes de planejar, editar vários arquivos, executar testes e criar pull requests. Este tutorial aborda as três principais ferramentas: Claude Code, Cursor e GitHub Copilot.</p>
<h2>1) O que torna uma ferramenta &quot;agêntica&quot;?</h2>
<p>O preenchimento automático tradicional com IA sugere as próximas linhas. As ferramentas agênticas vão além:</p>
<table>
<thead>
<tr>
<th>Capacidade</th>
<th>Preenchimento automático</th>
<th>Agêntica</th>
</tr>
</thead>
<tbody>
<tr>
<td>Escopo</td>
<td>Linha/função atual</td>
<td>Todo o código-fonte</td>
</tr>
<tr>
<td>Planejamento</td>
<td>Nenhum</td>
<td>Raciocínio em várias etapas</td>
</tr>
<tr>
<td>Execução</td>
<td>Apenas sugestões</td>
<td>Pode editar arquivos e executar comandos</td>
</tr>
<tr>
<td>Contexto</td>
<td>Arquivo atual</td>
<td>Compreensão de todo o repositório</td>
</tr>
<tr>
<td>Fluxo de trabalho</td>
<td>Reativo</td>
<td>Conclusão autônoma de tarefas</td>
</tr>
</tbody>
</table>
<p>As ferramentas agênticas podem compreender toda a estrutura do seu projeto, planejar alterações em vários arquivos, executar essas alterações, rodar testes e iterar até que a tarefa seja concluída.</p>
<h2>2) Comparação rápida: Claude Code vs Cursor vs Copilot</h2>
<table>
<thead>
<tr>
<th>Recurso</th>
<th>Claude Code</th>
<th>Cursor</th>
<th>GitHub Copilot</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Interface</strong></td>
<td>CLI, web, plugins para IDE</td>
<td>IDE completa (fork do VS Code)</td>
<td>Extensões para IDE</td>
</tr>
<tr>
<td><strong>Janela de contexto</strong></td>
<td>Grande (Opus com contexto longo)</td>
<td>Indexação de todo o repositório</td>
<td>Menor, em evolução</td>
</tr>
<tr>
<td><strong>Edições em vários arquivos</strong></td>
<td>Muito bom</td>
<td>Excelente</td>
<td>Em evolução</td>
</tr>
<tr>
<td><strong>Modo de planejamento</strong></td>
<td>Sim</td>
<td>Sim</td>
<td>Sim (mais recente)</td>
</tr>
<tr>
<td><strong>Preço</strong></td>
<td>Planos Pro/Max</td>
<td>Créditos + assinatura</td>
<td>Assinatura</td>
</tr>
<tr>
<td><strong>Ideal para</strong></td>
<td>Arquitetura, grandes refatorações</td>
<td>Desenvolvimento interativo</td>
<td>Programação inline</td>
</tr>
</tbody>
</table>
<h2>3) Fundamentos do Claude Code</h2>
<p>O Claude Code usa o Opus 4.8 (seu modelo padrão atual), com grandes janelas de contexto. Instale e autentique:</p>
<pre><code class="language-bash"># Instale o Claude Code (instalador nativo, recomendado)
curl -fsSL https://claude.ai/install.sh | bash

# Ou via npm (requer Node 18+)
npm install -g @anthropic-ai/claude-code

# Autentique-se: execute claude e siga a solicitação no navegador
claude

# Inicie no seu projeto
cd your-project
claude
</code></pre>
<h3>Comandos básicos</h3>
<pre><code class="language-bash"># Pergunte sobre seu código-fonte
claude &quot;Como a autenticação é implementada aqui?&quot;

# Gere código
claude &quot;Adicione um middleware de limitação de taxa&quot;

# Refatore vários arquivos
claude &quot;Renomeie UserService para AuthService em todo o código-fonte&quot;

# Crie um PR
claude &quot;Crie um PR que adicione validação de entrada a todos os endpoints da API&quot;
</code></pre>
<h3>Configuração do projeto</h3>
<p>Coloque o contexto do projeto em um arquivo <code>CLAUDE.md</code> na raiz do repositório. O Claude Code o lê automaticamente no início de cada sessão:</p>
<pre><code class="language-markdown"># Observações do projeto

- Linguagem: TypeScript, formatado com Prettier
- Testes: Vitest, em tests/
- Não altere node_modules/ nem dist/
</code></pre>
<p>As configurações legíveis por máquina (permissões de ferramentas, variáveis de ambiente e hooks) ficam em <code>.claude/settings.json</code>. Por exemplo, para exigir aprovação antes da execução de ferramentas destrutivas, configure as permissões nesse arquivo.</p>
<h3>Memória e contexto</h3>
<p>O contexto persistente do projeto fica em arquivos <a href="http://CLAUDE.md">CLAUDE.md</a>, que o Claude Code carrega no início de cada sessão, além de um armazenamento de memória automática que o próprio Claude atualiza com base nas suas correções e preferências. Para fornecer contexto permanente, edite o <a href="http://CLAUDE.md">CLAUDE.md</a> em vez de enviar uma instrução avulsa como &quot;Lembre-se:&quot;:</p>
<pre><code class="language-markdown"># CLAUDE.md
Este é um monorepo. O código compartilhado fica em packages/.
</code></pre>
<p>Use o comando <code>/clear</code> durante uma sessão para redefinir o contexto de trabalho.</p>
<h2>4) Recursos agênticos da IDE Cursor</h2>
<p>O Cursor foi desenvolvido em torno de fluxos de trabalho agênticos. Principais recursos:</p>
<h3>Modo Agent</h3>
<p>Pressione <code>Cmd+Shift+I</code> (Mac) ou <code>Ctrl+Shift+I</code> (Windows/Linux) para abrir o Composer no modo Agent:</p>
<pre><code>Fluxo de trabalho do modo Agent:
1. Descreva sua tarefa
2. O agente cria um plano
3. Você aprova ou modifica
4. O agente executa as alterações
5. Revise e itere
</code></pre>
<h3>Modo Plan</h3>
<p>Para tarefas complexas, use explicitamente o modo Plan:</p>
<pre><code>@plan Adicione autenticação de usuário com:
- Tokens JWT
- Rotação de refresh tokens
- Hash de senhas com bcrypt
- Limitação de tentativas de login
</code></pre>
<p>O agente irá:</p>
<ol>
<li>Analisar seu código existente</li>
<li>Criar um plano de implementação detalhado</li>
<li>Aguardar sua aprovação</li>
<li>Executar o plano passo a passo</li>
</ol>
<h3>Injeção de contexto</h3>
<p>Use <code>@</code> para adicionar um contexto específico:</p>
<pre><code>@file src/api/auth.ts
@folder src/middleware
@docs https://jwt.io/introduction

Implemente um middleware de verificação de JWT seguindo estes padrões
</code></pre>
<h3>Agentes em paralelo</h3>
<p>Execute vários agentes simultaneamente em branches diferentes:</p>
<pre><code># No Cursor:
1. Abra a Paleta de Comandos
2. &quot;Cursor: New Agent in Worktree&quot;
3. Cada agente trabalha isoladamente
4. Compare os resultados lado a lado
</code></pre>
<p>O Cursor 3 adicionou uma janela de agentes (abra-a com Cmd+Shift+P e depois &quot;Agents Window&quot;) que executa vários agentes em paralelo entre checkouts locais, worktrees do git e ambientes na nuvem. O comando /worktree coloca cada agente em seu próprio worktree isolado do git, enquanto /best-of-n executa a mesma tarefa em vários modelos para que você possa comparar os resultados lado a lado.</p>
<h3>Regras para manter a consistência</h3>
<p>Crie <code>.cursor/rules/project.mdc</code>:</p>
<pre><code class="language-markdown"># Regras do projeto

## Estilo do código
- Use o modo estrito do TypeScript
- Prefira componentes funcionais
- Use exportações nomeadas, não padrão

## Testes
- Todo recurso precisa de testes
- Use blocos describe/it
- Simule serviços externos

## Segurança
- Nunca faça commit de segredos
- Valide todas as entradas do usuário
- Use consultas parametrizadas
</code></pre>
<h2>5) Recursos agênticos do GitHub Copilot</h2>
<p>O Copilot evoluiu para além do preenchimento automático:</p>
<h3>Copilot Chat</h3>
<p>Abra o painel de chat e use comandos com barra:</p>
<pre><code>/explain - Explique o código selecionado
/fix - Corrija problemas na seleção
/tests - Gere testes
/doc - Adicione documentação
</code></pre>
<h3>Agente de programação (de issue a pull request)</h3>
<pre><code># No GitHub:
1. Abra uma issue do GitHub
2. Atribua-a ao &quot;Copilot&quot;
3. O agente de programação trabalha em segundo plano:
   analisa a issue, edita arquivos e executa testes
4. Ele abre um pull request para sua revisão
</code></pre>
<p>Isso é diferente do modo Agent do Copilot na IDE, que trabalha de forma síncrona ao seu lado no VS Code ou no JetBrains.</p>
<p>O Copilot agora divide seu trabalho autônomo em duas partes: o modo Agent é executado de forma síncrona dentro da sua IDE (VS Code e JetBrains), decidindo quais arquivos editar, executando comandos no terminal e iterando sobre erros; o agente de programação é executado de forma assíncrona na nuvem, assumindo uma issue atribuída e entregando um pull request enquanto você trabalha em outra coisa.</p>
<h3>Sugestões inline com contexto</h3>
<p>O Copilot lê seu código-fonte. Ajude-o com comentários:</p>
<pre><code class="language-typescript">// Middleware de autenticação que:
// - Extrai o JWT do cabeçalho Authorization
// - Valida a assinatura do token
// - Anexa o usuário à requisição
// - Retorna 401 se for inválido
export function authMiddleware(req, res, next) {
  // O Copilot gera a implementação
}
</code></pre>
<h2>6) Boas práticas para todas as ferramentas</h2>
<h3>Escreva prompts claros</h3>
<pre><code>❌ Ruim: &quot;Corrija o bug&quot;

✅ Bom: &quot;Corrija a exceção de ponteiro nulo em UserService.getById 
quando o usuário não existir. Retorne null em vez de lançar uma exceção.&quot;

❌ Ruim: &quot;Adicione testes&quot;

✅ Bom: &quot;Adicione testes unitários para o CartService cobrindo:
- Adição de itens a um carrinho vazio
- Remoção de itens
- Cálculo de totais com descontos
- Caso extremo: quantidades negativas&quot;
</code></pre>
<h3>Use o modo de planejamento para tarefas complexas</h3>
<pre><code>Para qualquer tarefa que altere 3 ou mais arquivos, sempre:

1. Peça primeiro um plano
2. Revise o plano
3. Aprove ou modifique
4. Deixe o agente executar
5. Revise as alterações antes de fazer commit
</code></pre>
<h3>Forneça contexto arquitetural</h3>
<pre><code>Antes de trabalhos importantes, contextualize o agente:

&quot;Esta é uma arquitetura de microsserviços:
- api-gateway: Encaminha requisições
- auth-service: JWT, sessões
- user-service: CRUD de usuários
- notification-service: E-mail, push

Os serviços se comunicam via pub/sub do Redis.
O banco de dados é PostgreSQL com o ORM Prisma.&quot;
</code></pre>
<h3>Desenvolvimento orientado a testes com agentes</h3>
<pre><code>1. Escreva primeiro um teste que falhe:
   &quot;Escreva um teste de cadastro de usuário que exija a validação do e-mail&quot;

2. Deixe o agente ver o teste falhando:
   &quot;Implemente o cadastro para fazer este teste passar&quot;

3. Continue iterando até tudo passar:
   &quot;Corrija as falhas de teste restantes&quot;
</code></pre>
<h2>7) Considerações de segurança</h2>
<p>As ferramentas agênticas têm poder real. Defina limites:</p>
<h3>Etapas de aprovação</h3>
<pre><code class="language-json">// .claude/config.json
{
  &quot;safety&quot;: {
    &quot;requireApproval&quot;: [
      &quot;rm&quot;,
      &quot;git push&quot;,
      &quot;npm publish&quot;,
      &quot;database migrations&quot;,
      &quot;env file changes&quot;
    ]
  }
}
</code></pre>
<h3>Sandboxing</h3>
<pre><code class="language-bash"># Execute agentes em ambientes isolados
docker run -v $(pwd):/workspace -it claude-sandbox

# Ou use worktrees do git para isolamento
git worktree add ../feature-branch feature-branch
</code></pre>
<h3>Arquivos confidenciais</h3>
<pre><code># .cursorignore / .claudeignore
.env
.env.*
secrets/
credentials/
*.pem
*.key
</code></pre>
<h3>Revise todas as alterações</h3>
<pre><code class="language-bash"># Antes de aceitar as alterações do agente:
git diff --stat
git diff

# Use o staging interativo
git add -p

# Nunca faça merge automático sem revisão
</code></pre>
<h2>8) Padrões de fluxo de trabalho que funcionam</h2>
<h3>Padrão 1: Explorar, planejar, executar</h3>
<pre><code>Sessão 1: &quot;Como funciona o sistema de pagamentos?&quot;
          → O agente explora, você aprende

Sessão 2: &quot;O que seria necessário para adicionar o Stripe?&quot;
          → O agente cria um plano

Sessão 3: &quot;Implemente a integração com o Stripe&quot;
          → O agente executa o plano
</code></pre>
<h3>Padrão 2: Exploração em paralelo</h3>
<pre><code># No Cursor com agentes em paralelo:

Agente 1: &quot;Implemente cache com Redis&quot;
Agente 2: &quot;Implemente cache com Memcached&quot;
Agente 3: &quot;Implemente cache com LRU em memória&quot;

→ Compare as implementações e escolha a melhor abordagem
</code></pre>
<h3>Padrão 3: Migração incremental</h3>
<pre><code>Etapa 1: &quot;Identifique todos os usos da API obsoleta&quot;
Etapa 2: &quot;Crie um adaptador para a nova API&quot;
Etapa 3: &quot;Migre o módulo A para usar o adaptador&quot;
Etapa 4: &quot;Migre o módulo B...&quot;
...
Etapa N: &quot;Remova o adaptador e use a nova API diretamente&quot;
</code></pre>
<h3>Padrão 4: Desenvolvimento de recursos começando pelos testes</h3>
<pre><code>1. &quot;Escreva testes de aceitação para a edição do perfil de usuário&quot;
2. &quot;Escreva testes unitários para a validação do perfil&quot;
3. &quot;Implemente para fazer os testes passarem&quot;
4. &quot;Adicione testes de integração&quot;
5. &quot;Refatore para melhorar a clareza&quot;
</code></pre>
<h2>9) Quando usar cada ferramenta</h2>
<table>
<thead>
<tr>
<th>Situação</th>
<th>Ferramenta recomendada</th>
</tr>
</thead>
<tbody>
<tr>
<td>Programação em tempo real, ajuda inline</td>
<td>GitHub Copilot</td>
</tr>
<tr>
<td>Grandes refatorações, arquitetura</td>
<td>Claude Code</td>
</tr>
<tr>
<td>Sessão completa de desenvolvimento</td>
<td>Cursor</td>
</tr>
<tr>
<td>Perguntas rápidas sobre código</td>
<td>Qualquer interface de chat</td>
</tr>
<tr>
<td>Criação de PRs a partir de issues</td>
<td>Copilot Workspace</td>
</tr>
<tr>
<td>Alterações complexas em vários arquivos</td>
<td>Cursor Agent</td>
</tr>
<tr>
<td>Design de API, documentação</td>
<td>Claude Code</td>
</tr>
<tr>
<td>Aprendizado de um novo código-fonte</td>
<td>Cursor ou Claude Code</td>
</tr>
</tbody>
</table>
<h2>10) Como medir a eficácia</h2>
<p>Acompanhe estas métricas para avaliar as ferramentas agênticas:</p>
<pre><code class="language-javascript">const metrics = {
  // Produtividade
  suggestionsAccepted: 'porcentagem de sugestões que você manteve',
  timeToFeature: 'horas desde o início até o merge do PR',
  linesPerHour: 'velocidade de produção de código (use com cuidado)',
  
  // Qualidade
  bugsIntroduced: 'defeitos atribuídos a código gerado por IA',
  testCoverage: 'cobertura do código gerado por IA',
  reviewChanges: 'quanto você modifica as sugestões da IA',
  
  // Custo
  apiCosts: 'gasto mensal com ferramentas de IA',
  creditsUsed: 'créditos do Cursor consumidos',
  
  // Aprendizado
  newPatternsLearned: 'técnicas que você aprendeu com a IA',
  codebaseUnderstanding: 'quanto a IA ajudou você a aprender',
}
</code></pre>
<h2>11) Armadilhas comuns</h2>
<h3>Confiar demais no agente</h3>
<pre><code>❌ Aceitar grandes alterações sem revisão
❌ Permitir que agentes executem comandos destrutivos
❌ Não entender o código antes de publicá-lo

✅ Revise cada alteração
✅ Entenda o que você publica
✅ Use agentes como colaboradores, não como substitutos
</code></pre>
<h3>Poluição de contexto</h3>
<pre><code>❌ Prompts enormes com contexto irrelevante
❌ Não limpar o contexto entre tarefas não relacionadas
❌ Incluir arquivos gerados no contexto

✅ Mantenha os prompts focados
✅ Use referências específicas com @file
✅ Exclua artefatos de build
</code></pre>
<h3>Ambiguidade no prompt</h3>
<pre><code>❌ &quot;Melhore isso&quot;
❌ &quot;Corrija os problemas&quot;
❌ &quot;Adicione alguns testes&quot;

✅ &quot;Reduza a complexidade de tempo de O(n²) para O(n log n)&quot;
✅ &quot;Trate o caso null na linha 45 retornando antecipadamente&quot;
✅ &quot;Adicione testes para casos extremos: entrada vazia, tamanho máximo, unicode&quot;
</code></pre>
<h2>12) Checklist para começar</h2>
<pre><code class="language-markdown">## Checklist de configuração

### Claude Code
- [ ] Instale a CLI: `npm install -g @anthropic/claude-code`
- [ ] Autentique-se: `claude login`
- [ ] Crie `.claude/config.json`
- [ ] Adicione `.claudeignore` para arquivos confidenciais

### Cursor
- [ ] Baixe em cursor.com
- [ ] Importe as configurações do VS Code
- [ ] Configure `.cursor/rules/`
- [ ] Aprenda a usar `Cmd+Shift+I` para o modo Agent

### GitHub Copilot
- [ ] Instale a extensão na sua IDE
- [ ] Ative o Copilot Chat
- [ ] Experimente o Copilot Workspace para issues
- [ ] Configure `.github/copilot-instructions.md`

### Todas as ferramentas
- [ ] Configure etapas de aprovação para operações destrutivas
- [ ] Crie arquivos de exclusão para segredos
- [ ] Estabeleça um fluxo de trabalho de revisão
- [ ] Acompanhe métricas desde o primeiro dia
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rapidamente</a></li>
<li><a href="/pt-BR/tutorials/web-workers-game-logic">Web Workers para a lógica do jogo</a></li>
<li><a href="/pt-BR/guides/game-jams-hackathons">Game jams e hackathons</a> — as ferramentas de IA para código estão mudando o que é possível fazer em uma jam de 48 horas</li>
<li><a href="/pt-BR/guides/web-game-engines-comparison">Comparação entre engines de jogos web</a> — escolha uma engine que combine bem com fluxos de trabalho assistidos por IA</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Analytics e telemetria para jogos web]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/analytics-web-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/analytics-web-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Acompanhe o que importa no seu jogo: comportamento dos jogadores, desempenho, erros e conversões. Tome decisões orientadas por dados.]]></description>
            <content:encoded><![CDATA[<h1>Analytics e telemetria para jogos web</h1>
<p>As ferramentas de analytics ajudam você a entender o que os jogadores fazem, onde encontram dificuldades e o que mantém o interesse deles. Este tutorial mostra o que acompanhar e como fazer isso.</p>
<h2>1) O que acompanhar</h2>
<p><strong>Métricas de engajamento:</strong></p>
<ul>
<li>Inícios e duração das sessões</li>
<li>Fases iniciadas e concluídas</li>
<li>Recursos utilizados</li>
<li>Retenção (visitas recorrentes)</li>
</ul>
<p><strong>Métricas de desempenho:</strong></p>
<ul>
<li>Tempo de carregamento</li>
<li>Taxa de quadros</li>
<li>Uso de memória</li>
<li>Erros e travamentos</li>
</ul>
<p><strong>Métricas de conversão:</strong></p>
<ul>
<li>Conclusão do tutorial</li>
<li>Primeira compra</li>
<li>Compartilhamentos em redes sociais</li>
</ul>
<h2>2) Rastreamento simples de eventos</h2>
<pre><code class="language-js">class Analytics {
  constructor(endpoint) {
    this.endpoint = endpoint
    this.sessionId = crypto.randomUUID()
    this.queue = []
    this.flushInterval = 30000 // 30 seconds
    
    setInterval(() =&gt; this.flush(), this.flushInterval)
    // Flush when the page is being hidden, not on beforeunload.
    // beforeunload/unload are unreliable (often don't fire on mobile,
    // and block the back/forward cache). visibilitychange + pagehide
    // are the recommended events for end-of-session sends.
    document.addEventListener('visibilitychange', () =&gt; {
      if (document.visibilityState === 'hidden') this.flush()
    })
    window.addEventListener('pagehide', () =&gt; this.flush())
  }
  
  track(event, data = {}) {
    this.queue.push({
      event,
      data,
      sessionId: this.sessionId,
      timestamp: Date.now(),
      url: location.href,
    })
    
    // Flush immediately for important events
    if (event === 'error' || event === 'purchase') {
      this.flush()
    }
  }
  
  async flush() {
    if (this.queue.length === 0) return
    
    const events = [...this.queue]
    this.queue = []
    
    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ events }),
        keepalive: true, // Important for beforeunload
      })
    } catch {
      // Put events back in queue
      this.queue.unshift(...events)
    }
  }
}

const analytics = new Analytics('/api/analytics')
</code></pre>
<h2>3) Rastreamento de sessões</h2>
<pre><code class="language-js">// Track session start
analytics.track('session_start', {
  referrer: document.referrer,
  screen: `${screen.width}x${screen.height}`,
  devicePixelRatio: window.devicePixelRatio,
  userAgent: navigator.userAgent,
})

// Track session end
let sessionStart = Date.now()

// Send session_end when the page is hidden, not on beforeunload.
// beforeunload is unreliable on mobile and breaks the bfcache.
function recordSessionEnd() {
  analytics.track('session_end', {
    duration: Date.now() - sessionStart,
  })
}
document.addEventListener('visibilitychange', () =&gt; {
  if (document.visibilityState === 'hidden') recordSessionEnd()
})
window.addEventListener('pagehide', recordSessionEnd)

// Track visibility changes
document.addEventListener('visibilitychange', () =&gt; {
  if (document.hidden) {
    analytics.track('tab_hidden')
  } else {
    analytics.track('tab_visible')
  }
})
</code></pre>
<h2>4) Eventos específicos do jogo</h2>
<pre><code class="language-js">// Level tracking
function onLevelStart(levelId) {
  analytics.track('level_start', { levelId })
}

function onLevelComplete(levelId, score, time) {
  analytics.track('level_complete', {
    levelId,
    score,
    timeSeconds: time,
  })
}

function onLevelFail(levelId, reason) {
  analytics.track('level_fail', {
    levelId,
    reason, // 'death', 'timeout', 'quit'
  })
}

// Achievement tracking
function onAchievementUnlocked(achievementId) {
  analytics.track('achievement', { achievementId })
}

// Tutorial tracking
function onTutorialStep(step, skipped = false) {
  analytics.track('tutorial', { step, skipped })
}
</code></pre>
<h2>5) Monitoramento de desempenho</h2>
<pre><code class="language-js">class PerformanceMonitor {
  constructor(analytics) {
    this.analytics = analytics
    this.frameTimes = []
    this.lastFrame = performance.now()
  }
  
  recordFrame() {
    const now = performance.now()
    this.frameTimes.push(now - this.lastFrame)
    this.lastFrame = now
    
    // Keep last 60 frames
    if (this.frameTimes.length &gt; 60) {
      this.frameTimes.shift()
    }
  }
  
  getAverageFPS() {
    if (this.frameTimes.length === 0) return 0
    const avgFrameTime = this.frameTimes.reduce((a, b) =&gt; a + b) / this.frameTimes.length
    return 1000 / avgFrameTime
  }
  
  reportPerformance() {
    const fps = this.getAverageFPS()
    const memory = performance.memory?.usedJSHeapSize
    
    this.analytics.track('performance', {
      avgFPS: Math.round(fps),
      memoryMB: memory ? Math.round(memory / 1024 / 1024) : null,
    })
  }
}

// Report every minute
const perfMonitor = new PerformanceMonitor(analytics)
setInterval(() =&gt; perfMonitor.reportPerformance(), 60000)
</code></pre>
<h3>Identifique a origem dos engasgos com Long Animation Frames</h3>
<p>O contador de FPS acima informa <em>quando</em> há quedas de quadros, mas não <em>por quê</em>. A API Long Animation Frames (LoAF), lançada no Chrome e no Edge 123, preenche essa lacuna. Ela sinaliza qualquer quadro que leve mais de 50 ms e detalha quais scripts causaram o atraso, permitindo atribuir os engasgos a trechos específicos de código em vez de tentar adivinhar.</p>
<pre><code class="language-js">if (PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')) {
  const observer = new PerformanceObserver((list) =&gt; {
    for (const entry of list.getEntries()) {
      analytics.track('long_frame', {
        durationMs: Math.round(entry.duration),
        blockingMs: Math.round(entry.blockingDuration),
        // scripts[] names the source URLs that ate the frame
        scripts: entry.scripts?.map((s) =&gt; s.sourceURL),
      })
    }
  })
  observer.observe({ type: 'long-animation-frame', buffered: true })
}
</code></pre>
<p>Por enquanto, a LoAF está disponível apenas em navegadores baseados em Chromium (não no Firefox nem no Safari). Portanto, detecte a compatibilidade antes de usá-la e mantenha o contador de FPS como referência compatível com diferentes navegadores.</p>
<h2>6) Rastreamento de erros</h2>
<pre><code class="language-js">window.addEventListener('error', (event) =&gt; {
  analytics.track('error', {
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    stack: event.error?.stack,
  })
})

window.addEventListener('unhandledrejection', (event) =&gt; {
  analytics.track('error', {
    message: event.reason?.message || String(event.reason),
    type: 'unhandledrejection',
    stack: event.reason?.stack,
  })
})

// Custom error tracking
function trackGameError(context, error) {
  analytics.track('game_error', {
    context,
    message: error.message,
    stack: error.stack,
  })
}
</code></pre>
<h2>7) Rastreamento do tempo de carregamento</h2>
<pre><code class="language-js">// Track initial load
window.addEventListener('load', () =&gt; {
  const timing = performance.timing
  const loadTime = timing.loadEventEnd - timing.navigationStart
  const domReady = timing.domContentLoadedEventEnd - timing.navigationStart
  
  analytics.track('page_load', {
    totalMs: loadTime,
    domReadyMs: domReady,
  })
})

// Track game-specific load phases
async function loadGame() {
  const start = performance.now()
  
  await loadCriticalAssets()
  const criticalTime = performance.now() - start
  
  analytics.track('load_critical', { ms: Math.round(criticalTime) })
  
  await loadGameAssets()
  const totalTime = performance.now() - start
  
  analytics.track('load_complete', { ms: Math.round(totalTime) })
}
</code></pre>
<h2>8) Rastreamento de funil</h2>
<p>Acompanhe o progresso dos jogadores pelos fluxos principais:</p>
<pre><code class="language-js">class FunnelTracker {
  constructor(analytics, funnelName) {
    this.analytics = analytics
    this.funnelName = funnelName
    this.startTime = Date.now()
  }
  
  step(stepName) {
    this.analytics.track('funnel_step', {
      funnel: this.funnelName,
      step: stepName,
      elapsedMs: Date.now() - this.startTime,
    })
  }
  
  complete() {
    this.analytics.track('funnel_complete', {
      funnel: this.funnelName,
      totalMs: Date.now() - this.startTime,
    })
  }
  
  abandon(reason) {
    this.analytics.track('funnel_abandon', {
      funnel: this.funnelName,
      reason,
      elapsedMs: Date.now() - this.startTime,
    })
  }
}

// Usage
const onboarding = new FunnelTracker(analytics, 'onboarding')
onboarding.step('welcome_shown')
// ... player clicks continue
onboarding.step('name_entered')
// ... player completes tutorial
onboarding.complete()
</code></pre>
<h2>9) Suporte a testes A/B</h2>
<pre><code class="language-js">class ABTest {
  constructor(testName, variants) {
    this.testName = testName
    this.variants = variants
    
    // Get or assign variant
    const stored = localStorage.getItem(`ab_${testName}`)
    if (stored &amp;&amp; variants.includes(stored)) {
      this.variant = stored
    } else {
      this.variant = variants[Math.floor(Math.random() * variants.length)]
      localStorage.setItem(`ab_${testName}`, this.variant)
    }
    
    // Track assignment
    analytics.track('ab_assignment', {
      test: testName,
      variant: this.variant,
    })
  }
  
  getVariant() {
    return this.variant
  }
  
  trackConversion(metric) {
    analytics.track('ab_conversion', {
      test: this.testName,
      variant: this.variant,
      metric,
    })
  }
}

// Usage
const difficultyTest = new ABTest('difficulty', ['easy', 'normal', 'hard'])
game.difficulty = difficultyTest.getVariant()

// When player completes level
difficultyTest.trackConversion('level_complete')
</code></pre>
<h2>10) Considerações sobre privacidade</h2>
<pre><code class="language-js">class PrivacyAwareAnalytics extends Analytics {
  constructor(endpoint) {
    super(endpoint)
    this.enabled = this.checkConsent()
  }
  
  checkConsent() {
    return localStorage.getItem('analytics_consent') === 'true'
  }
  
  setConsent(enabled) {
    localStorage.setItem('analytics_consent', enabled ? 'true' : 'false')
    this.enabled = enabled
    
    if (enabled) {
      this.track('consent_granted')
    }
  }
  
  track(event, data = {}) {
    if (!this.enabled) return
    
    // Strip PII
    const sanitized = { ...data }
    delete sanitized.email
    delete sanitized.name
    delete sanitized.ip
    
    super.track(event, sanitized)
  }
}

// Show consent dialog
function showConsentDialog() {
  const dialog = document.createElement('div')
  dialog.innerHTML = `
    &lt;p&gt;Usamos dados de analytics para melhorar o jogo. Tudo bem para você?&lt;/p&gt;
    &lt;button id=&quot;accept&quot;&gt;Aceitar&lt;/button&gt;
    &lt;button id=&quot;decline&quot;&gt;Recusar&lt;/button&gt;
  `
  document.body.appendChild(dialog)
  
  dialog.querySelector('#accept').onclick = () =&gt; {
    analytics.setConsent(true)
    dialog.remove()
  }
  
  dialog.querySelector('#decline').onclick = () =&gt; {
    analytics.setConsent(false)
    dialog.remove()
  }
}
</code></pre>
<h2>Alternativas de terceiros</h2>
<p>Se você não quiser criar sua própria solução:</p>
<ul>
<li><strong>Plausible</strong> — Simples e focado em privacidade</li>
<li><strong>Amplitude</strong> — Analytics de produto e funis</li>
<li><strong>Mixpanel</strong> — Rastreamento de eventos e jornadas dos usuários</li>
<li><strong>Sentry</strong> — Específico para rastreamento de erros</li>
</ul>
<pre><code class="language-js">// Example: Plausible
const script = document.createElement('script')
script.defer = true
script.dataset.domain = 'yourgame.com'
script.src = 'https://plausible.io/js/plausible.js'
document.head.appendChild(script)

// Track custom events
window.plausible('level_complete', { props: { level: '1' } })
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carrega rápido</a></li>
<li><a href="/pt-BR/tutorials/indexeddb-game-saves">Salvamentos de jogos com IndexedDB</a></li>
<li><a href="/pt-BR/creators">Para criadores</a></li>
<li><a href="/pt-BR/guides/itch-io-launch-guide">Como lançar seu jogo no itch.io</a> — analytics do <a href="http://itch.io">itch.io</a> e rastreamento de downloads</li>
<li><a href="/pt-BR/guides/steam-next-fest-strategy">Estratégia para o Steam Next Fest</a> — medição de listas de desejos e taxas de conversão da demo</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://plausible.io/">Plausible Analytics</a> — analytics leve e focado em privacidade</li>
<li><a href="https://posthog.com/">PostHog</a> — analytics de produto de código aberto com rastreamento de eventos</li>
<li><a href="https://sentry.io/">Sentry</a> — monitoramento de erros e relatórios de travamentos para aplicativos web</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Fundamentos do Loop de Jogo com Canvas 2D]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/canvas-2d-game-loop</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/canvas-2d-game-loop</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Crie um loop de jogo sólido com Canvas 2D. Aborda temporização, passo de tempo fixo, entrada, renderização e os erros comuns que costumam causar problemas.]]></description>
            <content:encoded><![CDATA[<h1>Fundamentos do Loop de Jogo com Canvas 2D</h1>
<p>O loop de jogo é o coração do seu jogo. Ele é executado continuamente, atualizando o estado do jogo e desenhando quadros tão rápido quanto o navegador permite. Faça isso direito e todo o resto ficará mais fácil. Faça errado e você ficará procurando bugs que só aparecem em determinadas máquinas.</p>
<p><strong>Experimente (use as setas ou WASD):</strong></p>
<GameLoopDemo />
<h2>A versão mais simples</h2>
<p>Aqui está um loop de jogo reduzido ao essencial:</p>
<pre><code class="language-js">const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')

function gameLoop() {
  update()
  render()
  requestAnimationFrame(gameLoop)
}

function update() {
  // Game logic here
}

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  // Drawing here
}

gameLoop()
</code></pre>
<p>Isso funciona, mas há um problema. A função <code>update</code> não sabe quanto tempo passou desde o último quadro. Em um monitor de 144 Hz, ela é executada duas vezes mais rápido do que em um monitor de 60 Hz. A física do seu jogo se comportará de maneira diferente em máquinas diferentes.</p>
<h2>O delta time corrige problemas de taxa de quadros</h2>
<p>Para que seu jogo seja executado na mesma velocidade em qualquer lugar, você precisa acompanhar quanto tempo passou entre os quadros. Isso é chamado de delta time, geralmente abreviado como <code>dt</code>.</p>
<pre><code class="language-js">let lastTime = 0

function gameLoop(currentTime) {
  const dt = (currentTime - lastTime) / 1000 // convert to seconds
  lastTime = currentTime
  
  update(dt)
  render()
  
  requestAnimationFrame(gameLoop)
}

function update(dt) {
  player.x += player.speed * dt // now works at any frame rate
}

requestAnimationFrame(gameLoop)
</code></pre>
<p>O callback de <code>requestAnimationFrame</code> recebe o timestamp atual em milissegundos. Eu divido por 1.000 para obter segundos porque isso facilita os cálculos. Uma velocidade de 200 significa 200 pixels por segundo, o que é intuitivo.</p>
<p>Um detalhe importante: no primeiro quadro, <code>lastTime</code> é 0, então <code>dt</code> será enorme. Algumas pessoas inicializam <code>lastTime</code> com <code>performance.now()</code> antes de iniciar o loop ou ignoram a atualização do primeiro quadro.</p>
<h2>Por que usar requestAnimationFrame em vez de setInterval</h2>
<p>Você talvez esteja se perguntando por que usamos <code>requestAnimationFrame</code> em vez de <code>setInterval</code>. Há alguns motivos.</p>
<p>Primeiro, <code>requestAnimationFrame</code> sincroniza com a taxa de atualização da tela, proporcionando uma animação suave e sem tearing. Segundo, ele pausa automaticamente quando a aba fica oculta, economizando bateria e CPU. Terceiro, o navegador consegue otimizá-lo melhor.</p>
<p>Esse comportamento de pausa geralmente é o que você deseja, mas significa que o jogo congela quando a pessoa muda de aba. Se você estiver criando um jogo multijogador ou algo que precise continuar em execução, será necessário usar outra abordagem.</p>
<h2>Passo de tempo fixo para física</h2>
<p>O delta time variável funciona para movimentos simples, mas causa problemas em simulações de física. Se um quadro demorar demais, seu personagem poderá atravessar uma parede porque a verificação de colisão não detectou o momento do impacto.</p>
<p>A solução é usar um passo de tempo fixo. Você acumula o tempo e executa as atualizações de física em uma frequência fixa, independentemente da velocidade com que os quadros são renderizados.</p>
<pre><code class="language-js">const FIXED_DT = 1 / 60 // 60 physics updates per second
let accumulator = 0
let lastTime = 0

function gameLoop(currentTime) {
  const frameTime = Math.min((currentTime - lastTime) / 1000, 0.25)
  lastTime = currentTime
  
  accumulator += frameTime
  
  while (accumulator &gt;= FIXED_DT) {
    update(FIXED_DT)
    accumulator -= FIXED_DT
  }
  
  render()
  requestAnimationFrame(gameLoop)
}
</code></pre>
<p>Esse <code>Math.min(..., 0.25)</code> é importante. Se a aba estava oculta e voltar a ficar visível, <code>frameTime</code> poderá ser enorme. Sem esse limite, o loop while executaria centenas de atualizações para tentar compensar o atraso, travando o jogo. Limitar o valor a 0,25 segundo significa que você nunca executará mais de 15 atualizações de física por quadro.</p>
<h2>Organizando o estado do jogo</h2>
<p>À medida que seu jogo cresce, você precisa de um lugar para armazenar todos os dados. Gosto de usar um único objeto que contenha tudo:</p>
<pre><code class="language-js">const game = {
  state: 'playing', // 'menu', 'playing', 'paused', 'gameover'
  score: 0,
  player: {
    x: 100,
    y: 100,
    vx: 0,
    vy: 0,
    speed: 200,
    width: 32,
    height: 32,
  },
  entities: [],
}
</code></pre>
<p>Isso facilita salvar e carregar o estado do jogo, reiniciar quando o jogador morre e depurar registrando <code>game</code> no console. Algumas pessoas preferem classes, mas um objeto simples funciona bem para jogos pequenos e médios.</p>
<h2>Juntando tudo</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')

const FIXED_DT = 1 / 60
let accumulator = 0
let lastTime = 0

const input = { left: false, right: false, up: false, down: false }

const game = {
  player: { x: 100, y: 100, speed: 200 },
}

function update(dt) {
  const p = game.player
  
  if (input.left) p.x -= p.speed * dt
  if (input.right) p.x += p.speed * dt
  if (input.up) p.y -= p.speed * dt
  if (input.down) p.y += p.speed * dt
  
  // Keep in bounds
  p.x = Math.max(0, Math.min(canvas.width - 32, p.x))
  p.y = Math.max(0, Math.min(canvas.height - 32, p.y))
}

function render() {
  ctx.fillStyle = '#1a1a2e'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  
  ctx.fillStyle = '#4ade80'
  ctx.fillRect(game.player.x, game.player.y, 32, 32)
}

function gameLoop(currentTime) {
  const frameTime = Math.min((currentTime - lastTime) / 1000, 0.25)
  lastTime = currentTime
  
  accumulator += frameTime
  
  while (accumulator &gt;= FIXED_DT) {
    update(FIXED_DT)
    accumulator -= FIXED_DT
  }
  
  render()
  requestAnimationFrame(gameLoop)
}

// Input handling
window.addEventListener('keydown', (e) =&gt; {
  if (e.code === 'ArrowLeft' || e.code === 'KeyA') input.left = true
  if (e.code === 'ArrowRight' || e.code === 'KeyD') input.right = true
  if (e.code === 'ArrowUp' || e.code === 'KeyW') input.up = true
  if (e.code === 'ArrowDown' || e.code === 'KeyS') input.down = true
})

window.addEventListener('keyup', (e) =&gt; {
  if (e.code === 'ArrowLeft' || e.code === 'KeyA') input.left = false
  if (e.code === 'ArrowRight' || e.code === 'KeyD') input.right = false
  if (e.code === 'ArrowUp' || e.code === 'KeyW') input.up = false
  if (e.code === 'ArrowDown' || e.code === 'KeyS') input.down = false
})

requestAnimationFrame(gameLoop)
</code></pre>
<p>Este é um loop de jogo completo e executável. O jogador se move com as setas ou WASD, permanece dentro dos limites e tudo funciona em uma velocidade constante, independentemente da taxa de quadros. Você pode copiar este código como ponto de partida para qualquer jogo com Canvas 2D.</p>
<p>Observe como o tratamento de entrada está separado do loop de jogo. Os listeners de eventos apenas definem flags, e a função de atualização lê essas flags. Esse desacoplamento é importante porque os eventos de teclado são disparados em momentos imprevisíveis, enquanto você quer que a lógica do jogo seja executada em uma frequência previsível.</p>
<h2>Carregando e desenhando sprites</h2>
<pre><code class="language-js">const sprites = {}

async function loadSprite(name, url) {
  return new Promise((resolve) =&gt; {
    const img = new Image()
    img.onload = () =&gt; {
      sprites[name] = img
      resolve(img)
    }
    img.src = url
  })
}

function drawSprite(name, x, y, width, height) {
  const sprite = sprites[name]
  if (sprite) {
    ctx.drawImage(sprite, x, y, width || sprite.width, height || sprite.height)
  }
}
</code></pre>
<p>As imagens são carregadas de forma assíncrona, então você precisa esperar por elas antes de iniciar o jogo. Um padrão comum é carregar todos os sprites em uma função <code>loadAssets</code> que retorna uma Promise e iniciar o loop de jogo somente depois que ela for resolvida.</p>
<p>A função <code>drawSprite</code> verifica se o sprite existe antes de desenhá-lo. Isso evita erros caso você tente desenhar algo que ainda não foi carregado.</p>
<h2>Sprites animados</h2>
<pre><code class="language-js">class AnimatedSprite {
  constructor(image, frameWidth, frameHeight, frameCount, fps = 10) {
    this.image = image
    this.frameWidth = frameWidth
    this.frameHeight = frameHeight
    this.frameCount = frameCount
    this.frameDuration = 1 / fps
    this.currentFrame = 0
    this.elapsed = 0
  }
  
  update(dt) {
    this.elapsed += dt
    if (this.elapsed &gt;= this.frameDuration) {
      this.currentFrame = (this.currentFrame + 1) % this.frameCount
      this.elapsed = 0
    }
  }
  
  draw(ctx, x, y) {
    ctx.drawImage(
      this.image,
      this.currentFrame * this.frameWidth, 0,
      this.frameWidth, this.frameHeight,
      x, y,
      this.frameWidth, this.frameHeight
    )
  }
}
</code></pre>
<p>Essa classe pressupõe que sua sprite sheet tenha os quadros dispostos horizontalmente em uma única linha. Cada quadro tem o mesmo tamanho. O método <code>update</code> avança pelos quadros com base no tempo decorrido, e <code>draw</code> usa a versão de nove argumentos de <code>drawImage</code> para recortar apenas o quadro atual.</p>
<p>Você a usaria assim:</p>
<pre><code class="language-js">const playerWalk = new AnimatedSprite(walkImage, 32, 32, 4, 12)

// In update:
playerWalk.update(dt)

// In render:
playerWalk.draw(ctx, player.x, player.y)
</code></pre>
<h2>Detecção de colisão</h2>
<pre><code class="language-js">function rectCollision(a, b) {
  return (
    a.x &lt; b.x + b.width &amp;&amp;
    a.x + a.width &gt; b.x &amp;&amp;
    a.y &lt; b.y + b.height &amp;&amp;
    a.y + a.height &gt; b.y
  )
}

function circleCollision(a, b) {
  const dx = a.x - b.x
  const dy = a.y - b.y
  const distance = Math.sqrt(dx * dx + dy * dy)
  return distance &lt; a.radius + b.radius
}

// In update:
for (const enemy of game.enemies) {
  if (rectCollision(game.player, enemy)) {
    handleCollision(game.player, enemy)
  }
}
</code></pre>
<p>A colisão entre retângulos, também chamada de AABB (caixa delimitadora alinhada aos eixos), é a opção mais simples e rápida. Use-a para a maioria dos casos. A colisão entre círculos é melhor para objetos redondos ou quando você quer que as colisões sejam mais tolerantes.</p>
<p>Para poucas entidades, não há problema em verificar cada par. Se você tiver centenas de entidades, será melhor usar particionamento espacial, como quadtrees ou uma grade, mas esse é um assunto para outro tutorial.</p>
<h2>Uma câmera simples</h2>
<pre><code class="language-js">const camera = {
  x: 0,
  y: 0,
  
  follow(target) {
    this.x = target.x - canvas.width / 2
    this.y = target.y - canvas.height / 2
  }
}

function render() {
  ctx.save()
  ctx.translate(-camera.x, -camera.y)
  
  // Draw world
  drawWorld()
  drawEntities()
  
  ctx.restore()
  
  // Draw UI (not affected by camera)
  drawUI()
}
</code></pre>
<p>O segredo está em <code>ctx.save()</code> e <code>ctx.restore()</code>. Você salva o estado atual da transformação, aplica o deslocamento da câmera com <code>translate</code>, desenha tudo no espaço do mundo e então restaura o estado para que os elementos da interface não sejam afetados pela câmera.</p>
<p>Para suavizar o movimento da câmera, você pode interpolar em direção ao alvo em vez de saltar diretamente para ele. Algo como <code>this.x += (targetX - this.x) * 0.1</code> produz aquele agradável efeito de atraso.</p>
<h2>Pausando o jogo</h2>
<pre><code class="language-js">let paused = false

window.addEventListener('keydown', (e) =&gt; {
  if (e.code === 'Escape') {
    paused = !paused
  }
})

function gameLoop(currentTime) {
  const frameTime = Math.min((currentTime - lastTime) / 1000, 0.25)
  lastTime = currentTime
  
  if (!paused) {
    accumulator += frameTime
    while (accumulator &gt;= FIXED_DT) {
      update(FIXED_DT)
      accumulator -= FIXED_DT
    }
  }
  
  render()
  
  if (paused) {
    renderPauseOverlay()
  }
  
  requestAnimationFrame(gameLoop)
}

function renderPauseOverlay() {
  ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  
  ctx.fillStyle = '#fff'
  ctx.font = '32px sans-serif'
  ctx.textAlign = 'center'
  ctx.fillText('PAUSADO', canvas.width / 2, canvas.height / 2)
}
</code></pre>
<p>A ideia principal é parar de acumular tempo enquanto o jogo está pausado, mas continuar renderizando. Assim, a sobreposição de pausa aparece imediatamente e o jogo continua respondendo ao comando para retomar.</p>
<p>Você pode ampliar esse padrão para lidar com vários estados do jogo, como menus, cenas cinematográficas e telas de fim de jogo. Basta verificar o estado atual e direcionar a execução para diferentes funções de atualização e renderização.</p>
<h2>Erros comuns</h2>
<p>Alguns problemas que já vi confundirem muita gente:</p>
<p><strong>Esquecer de limpar o canvas.</strong> Se você não chamar <code>clearRect</code> no início da renderização, desenhará sobre o quadro anterior e produzirá rastros na imagem.</p>
<p><strong>Não limitar o delta time.</strong> Se alguém mudar de aba e voltar, um delta time enorme poderá quebrar a física ou fazer o jogo tentar &quot;compensar o atraso&quot; por vários segundos.</p>
<p><strong>Desenhar antes de as imagens carregarem.</strong> O Canvas ignora silenciosamente chamadas de <code>drawImage</code> com imagens que ainda não foram carregadas. Seus sprites simplesmente não aparecerão, sem nenhuma mensagem de erro.</p>
<p><strong>Usar <code>setInterval</code> para o loop.</strong> Ele não sincroniza com a tela nem pausa quando a aba fica oculta. Use <code>requestAnimationFrame</code>.</p>
<p><strong>Misturar coordenadas do mundo e da tela.</strong> Ao adicionar uma câmera, acompanhe se uma posição está no espaço do mundo ou no espaço da tela. Cliques do mouse chegam em coordenadas da tela e precisam ser convertidos.</p>
<h2>Próximos passos</h2>
<p>Este tutorial aborda os fundamentos. Quando você estiver familiarizado com esses padrões, talvez queira explorar:</p>
<p><a href="/pt-BR/tutorials/responsive-game-canvas">Canvas responsivo para jogos</a> para lidar com diferentes tamanhos de tela e telas de alta densidade de pixels.</p>
<p><a href="/pt-BR/tutorials/game-input-handling">Tratamento de entrada em jogos</a> para controles por toque, gamepads e sistemas de entrada mais sofisticados.</p>
<p><a href="/pt-BR/tutorials/pixel-art-rendering">Renderização de pixel art</a> para obter gráficos em pixel nítidos e sem desfoque.</p>
<p><a href="/pt-BR/tutorials/game-physics-libraries">Bibliotecas de física para jogos</a> para adicionar física de corpos rígidos, colisões e juntas ao loop do seu jogo.</p>
<p>Se você estiver decidindo entre Canvas 2D, WebGL e WebGPU para seu projeto, o guia <a href="/pt-BR/guides/web-games-stack-2026">Stack de tecnologias para jogos web em 2026</a> explica as vantagens e desvantagens de cada opção. A <a href="/pt-BR/guides/web-game-engines-comparison">Comparação de engines para jogos web</a> aborda frameworks que cuidam do loop de jogo para você.</p>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame">MDN: requestAnimationFrame</a> — referência da API do navegador</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API">MDN: API Canvas</a> — documentação completa do Canvas 2D</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent">MDN: KeyboardEvent</a> — códigos de teclas e propriedades de eventos</li>
<li><a href="https://gafferongames.com/post/fix_your_timestep/">Corrija seu timestep!</a> — artigo clássico de Glenn Fiedler sobre loops de jogos com timestep fixo</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Habilite threads Wasm (SharedArrayBuffer) com COOP/COEP]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/coop-coep-sharedarraybuffer</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/coop-coep-sharedarraybuffer</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Um guia prático, pronto para copiar e colar, sobre isolamento entre origens: cabeçalhos obrigatórios, problemas comuns e exemplos de configuração de servidor para threads com SharedArrayBuffer / WebAssembly. Inclui uma ferramenta gratuita para verificar se qualquer URL está isolada entre origens.]]></description>
            <content:encoded><![CDATA[<h1>Habilite threads Wasm (SharedArrayBuffer) com COOP/COEP</h1>
<p>Se o seu jogo para web (ou a exportação da sua engine) usa <strong>threads do WebAssembly</strong>, ele geralmente depende de <code>SharedArrayBuffer</code>. Os navegadores modernos exigem <strong>isolamento entre origens</strong> para isso.</p>
<p><img src="/img/cross-origin-isolation.svg" alt="Diagrama: cabeçalhos de isolamento entre origens"></p>
<h2>1) Verificação rápida: sua página está isolada?</h2>
<p>Abra o DevTools e execute:</p>
<pre><code class="language-js">self.crossOriginIsolated
</code></pre>
<p>Se o resultado for <code>true</code>, você está no estado de segurança correto para usar <code>SharedArrayBuffer</code>.</p>
<p>Referência oficial:</p>
<ul>
<li><code>https://developer.mozilla.org/docs/Web/API/Window/crossOriginIsolated</code></li>
</ul>
<h2>2) Os dois cabeçalhos necessários</h2>
<p>Envie estes cabeçalhos de resposta no seu HTML (e, normalmente, em todo o seu aplicativo):</p>
<pre><code class="language-http">Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
</code></pre>
<p>Referências oficiais:</p>
<ul>
<li><code>https://web.dev/articles/why-coop-coep</code></li>
<li><code>https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer</code></li>
<li><code>https://developer.chrome.com/blog/enabling-shared-array-buffer/</code></li>
</ul>
<h2>3) O principal motivo para “ainda não funcionar”: sub-recursos</h2>
<p>Com <code>COEP: require-corp</code>, sua página não pode mais incorporar livremente recursos de outras origens.</p>
<p>Se você carregar um script, imagem, worker, vídeo ou outro recurso de uma origem diferente, esse recurso precisará autorizar o uso por meio de:</p>
<ul>
<li>CORS (<code>Access-Control-Allow-Origin</code>) <strong>ou</strong></li>
<li>CORP (<code>Cross-Origin-Resource-Policy</code>)</li>
</ul>
<p>Referência oficial:</p>
<ul>
<li><code>https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cross-Origin_Resource_Policy</code></li>
</ul>
<h2>4) Configurações para copiar e colar</h2>
<h3>Nginx</h3>
<pre><code class="language-nginx">add_header Cross-Origin-Opener-Policy &quot;same-origin&quot; always;
add_header Cross-Origin-Embedder-Policy &quot;require-corp&quot; always;
</code></pre>
<h3>Node / Express</h3>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
  next()
})
</code></pre>
<h3>Cloudflare Pages (<code>_headers</code>)</h3>
<pre><code class="language-txt">/*
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp
</code></pre>
<h2>5) Checklist de depuração</h2>
<ul>
<li>Confirme se os cabeçalhos estão presentes na resposta do <strong>documento</strong> no painel Network.</li>
<li>Verifique <code>self.crossOriginIsolated</code>.</li>
<li>Procure solicitações bloqueadas (uma mensagem comum do DevTools inclui “blocked … by COEP”).</li>
<li>Remova ou hospede por conta própria scripts de terceiros que não sejam compatíveis com CORS/CORP.</li>
</ul>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo para web que carregue rapidamente</a></li>
<li><a href="/pt-BR/guides/web-games-stack-2026">Stack tecnológica para jogos web em 2026</a></li>
<li><a href="/pt-BR/tutorials/web-workers-game-logic">Web Workers para a lógica do jogo</a> — uso de Workers que se beneficiam do SharedArrayBuffer</li>
<li><a href="/pt-BR/tutorials/websocket-multiplayer-basics">Noções básicas de jogos multijogador com WebSocket</a> — jogos multijogador que precisam de isolamento entre origens</li>
<li><a href="/pt-BR/tutorials/iframe-embedding-games">Incorporação de jogos com iframe</a> — como os cabeçalhos COOP/COEP afetam incorporações com iframe</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy">MDN: Cross-Origin-Opener-Policy</a> — referência do cabeçalho COOP</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy">MDN: Cross-Origin-Embedder-Policy</a> — referência do cabeçalho COEP</li>
<li><a href="https://web.dev/articles/cross-origin-isolation-guide">web.dev: Como isolar seu site entre origens</a> — guia prático de implantação</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Manipulação de entrada em jogos (teclado, mouse, toque, controle)]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/game-input-handling</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/game-input-handling</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Manipulação unificada de entrada para jogos web: compatibilidade com teclado, mouse, toque e controle usando padrões práticos de código.]]></description>
            <content:encoded><![CDATA[<h1>Manipulação de Entrada em Jogos</h1>
<p>Uma boa manipulação de entrada pode determinar o sucesso ou o fracasso de um jogo. Controles com atraso, entradas não detectadas ou mapeamentos de botões confusos frustram os jogadores antes mesmo que eles vejam o conteúdo do seu jogo. Este tutorial mostra como lidar com teclado, mouse, toque e controle de forma unificada.</p>
<p><strong>Experimente primeiro:</strong></p>
<InputDemo />
<h2>O Padrão de Estado de Entrada</h2>
<p>A abordagem ingênua é reagir diretamente aos eventos: quando o usuário pressiona uma tecla, faça algo. Isso causa problemas. Os eventos são disparados em momentos imprevisíveis, enquanto o loop do jogo é executado em uma frequência fixa. Se você mover o jogador no manipulador de <code>keydown</code>, a velocidade do movimento dependerá da taxa de repetição das teclas, que varia conforme o sistema operacional.</p>
<p>Em vez disso, mantenha um objeto de estado de entrada que seja lido pelo loop do jogo:</p>
<pre><code class="language-js">const input = {
  left: false,
  right: false,
  up: false,
  down: false,
  action: false,
  pointer: { x: 0, y: 0, down: false },
}
</code></pre>
<p>Os manipuladores de eventos definem esses indicadores. O loop do jogo os lê. Esse desacoplamento simplifica tudo: sua lógica de atualização não precisa saber se a entrada veio do teclado, do toque ou do controle.</p>
<h2>Teclado</h2>
<p>Use <code>e.code</code> em vez de <code>e.key</code>, pois ele se refere à posição física da tecla. Assim, o WASD funciona corretamente em layouts diferentes do QWERTY.</p>
<pre><code class="language-js">const keyMap = {
  ArrowLeft: 'left',
  ArrowRight: 'right',
  ArrowUp: 'up',
  ArrowDown: 'down',
  KeyA: 'left',
  KeyD: 'right',
  KeyW: 'up',
  KeyS: 'down',
  Space: 'action',
}

window.addEventListener('keydown', (e) =&gt; {
  const action = keyMap[e.code]
  if (action) {
    input[action] = true
    e.preventDefault()
  }
})

window.addEventListener('keyup', (e) =&gt; {
  const action = keyMap[e.code]
  if (action) input[action] = false
})
</code></pre>
<p>O <code>e.preventDefault()</code> impede que as teclas de seta rolem a página. Chame-o apenas para as teclas que você está tratando.</p>
<h2>Mouse</h2>
<p>As coordenadas do mouse são fornecidas no espaço da tela. Você precisa convertê-las para o espaço do canvas:</p>
<pre><code class="language-js">const canvas = document.getElementById('game')

canvas.addEventListener('mousemove', (e) =&gt; {
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = e.clientX - rect.left
  input.pointer.y = e.clientY - rect.top
})

canvas.addEventListener('mousedown', () =&gt; {
  input.pointer.down = true
  input.action = true
})

canvas.addEventListener('mouseup', () =&gt; {
  input.pointer.down = false
  input.action = false
})
</code></pre>
<p>Se o seu canvas estiver redimensionado (consulte o tutorial sobre canvas responsivo), você precisará dividir pelo fator de escala para obter as coordenadas do jogo.</p>
<h2>Toque</h2>
<p>O toque funciona de forma semelhante ao mouse, mas você precisa usar <code>e.preventDefault()</code> para impedir que o navegador role a página ou aplique zoom:</p>
<pre><code class="language-js">canvas.addEventListener('touchstart', (e) =&gt; {
  e.preventDefault()
  const touch = e.touches[0]
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = touch.clientX - rect.left
  input.pointer.y = touch.clientY - rect.top
  input.pointer.down = true
  input.action = true
}, { passive: false })

canvas.addEventListener('touchmove', (e) =&gt; {
  e.preventDefault()
  const touch = e.touches[0]
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = touch.clientX - rect.left
  input.pointer.y = touch.clientY - rect.top
}, { passive: false })

canvas.addEventListener('touchend', () =&gt; {
  input.pointer.down = false
  input.action = false
})
</code></pre>
<p>O <code>{ passive: false }</code> é necessário porque, por padrão, os navegadores modernos usam listeners passivos para eventos de toque por motivos de desempenho. Sem isso, <code>preventDefault</code> não funcionará.</p>
<h2>Direcional Virtual para Dispositivos Móveis</h2>
<p>Para jogos que precisam de controles direcionais em dispositivos móveis, adicione botões à tela:</p>
<pre><code class="language-js">function createVirtualDpad(container) {
  const dpad = document.createElement('div')
  dpad.className = 'virtual-dpad'
  dpad.innerHTML = `
    &lt;button data-dir=&quot;up&quot;&gt;↑&lt;/button&gt;
    &lt;button data-dir=&quot;left&quot;&gt;←&lt;/button&gt;
    &lt;button data-dir=&quot;right&quot;&gt;→&lt;/button&gt;
    &lt;button data-dir=&quot;down&quot;&gt;↓&lt;/button&gt;
  `
  
  dpad.querySelectorAll('button').forEach(btn =&gt; {
    const dir = btn.dataset.dir
    btn.addEventListener('touchstart', (e) =&gt; {
      e.preventDefault()
      input[dir] = true
    })
    btn.addEventListener('touchend', () =&gt; input[dir] = false)
  })
  
  container.appendChild(dpad)
}
</code></pre>
<p>Estilize esses botões para que sejam grandes o suficiente para os polegares (pelo menos 44x44 pixels) e posicione-os nos cantos inferiores.</p>
<h2>Controle</h2>
<p>Ao contrário do teclado e do mouse, o estado do controle não é orientado por eventos. Você precisa consultá-lo a cada frame:</p>
<pre><code class="language-js">function pollGamepads() {
  const gamepads = navigator.getGamepads()
  const gp = gamepads[0]
  
  if (gp) {
    // Direcional ou analógico esquerdo
    input.left = gp.buttons[14]?.pressed || gp.axes[0] &lt; -0.5
    input.right = gp.buttons[15]?.pressed || gp.axes[0] &gt; 0.5
    input.up = gp.buttons[12]?.pressed || gp.axes[1] &lt; -0.5
    input.down = gp.buttons[13]?.pressed || gp.axes[1] &gt; 0.5
    
    // Botão A (mapeamento padrão)
    input.action = gp.buttons[0]?.pressed
  }
}
</code></pre>
<p>Chame <code>pollGamepads()</code> no início da sua função de atualização. O limite de 0,5 nos analógicos cria uma zona morta para que pequenos movimentos não sejam registrados como entrada.</p>
<p>Você pode detectar quando controles são conectados e desconectados:</p>
<pre><code class="language-js">window.addEventListener('gamepadconnected', (e) =&gt; {
  console.log('Controle conectado:', e.gamepad.id)
})

window.addEventListener('gamepaddisconnected', (e) =&gt; {
  console.log('Controle desconectado:', e.gamepad.id)
})
</code></pre>
<h2>Usando a Entrada no Loop do Jogo</h2>
<p>Agora, sua função de atualização apenas lê o estado de entrada:</p>
<pre><code class="language-js">function update(dt) {
  pollGamepads()
  
  if (input.left) player.x -= player.speed * dt
  if (input.right) player.x += player.speed * dt
  if (input.up) player.y -= player.speed * dt
  if (input.down) player.y += player.speed * dt
  
  if (input.action &amp;&amp; player.canShoot) {
    player.shoot()
  }
}
</code></pre>
<p>Isso funciona independentemente de a entrada ter vindo do teclado, do toque ou do controle. A lógica do jogo não precisa saber.</p>
<h2>Recém-Pressionado vs. Mantido Pressionado</h2>
<p>Às vezes, você quer detectar quando um botão acabou de ser pressionado neste frame, não se ele está sendo mantido pressionado. Pular é um bom exemplo: você quer um salto por pressionamento, não saltos contínuos enquanto o botão estiver pressionado.</p>
<pre><code class="language-js">const inputPrev = { ...input }

function wasJustPressed(action) {
  return input[action] &amp;&amp; !inputPrev[action]
}

function endFrame() {
  Object.assign(inputPrev, input)
}
</code></pre>
<p>Chame <code>endFrame()</code> no final do seu loop de atualização. Depois, use <code>wasJustPressed('action')</code> em vez de <code>input.action</code> para ações que devem acontecer uma única vez.</p>
<h2>Perda de Foco</h2>
<p>Quando a janela do navegador perde o foco, os eventos do teclado deixam de ser disparados. Se o jogador estava segurando uma tecla, ela fica presa no estado ativo:</p>
<pre><code class="language-js">window.addEventListener('blur', () =&gt; {
  input.left = input.right = input.up = input.down = input.action = false
})
</code></pre>
<p>Sem isso, o jogador continua se movendo quando o usuário alterna para outra janela com Alt+Tab.</p>
<h2>Boas Práticas</h2>
<p>Ofereça suporte tanto a WASD quanto às teclas de seta, pois os jogadores têm preferências diferentes. Permita remapear os controles e armazene a configuração no localStorage. Disponibilize controles por toque em dispositivos móveis. Teste com controles reais, pois os controles de Xbox, PlayStation e modelos genéricos se comportam de maneiras ligeiramente diferentes. E sempre trate a perda de foco adequadamente.</p>
<h2>Mais Recursos</h2>
<p><a href="/pt-BR/tutorials/gamepad-api">Análise detalhada da Gamepad API</a> aborda recursos avançados de controles, como vibração e suporte a vários jogadores.</p>
<p><a href="/pt-BR/tutorials/mobile-friendly-web-games">Jogos web adaptados para dispositivos móveis</a> aborda a entrada por toque em mais detalhes.</p>
<p><a href="/pt-BR/tutorials/pointer-lock-fps">Pointer Lock para jogos de FPS</a> mostra como capturar o mouse para controles em primeira pessoa.</p>
<p><a href="/pt-BR/tutorials/canvas-2d-game-loop">Loop de jogo com Canvas 2D</a> mostra o padrão de estado de entrada integrado a um loop de jogo completo.</p>
<p><a href="/pt-BR/tutorials/websocket-multiplayer-basics">Noções básicas de multijogador com WebSocket</a> aborda a sincronização de entradas entre jogadores conectados pela rede.</p>
<h2>Recursos Externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code">MDN: KeyboardEvent.code</a> — valores das teclas físicas para o mapeamento de entrada</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events">MDN: Eventos de ponteiro</a> — eventos unificados de mouse e toque</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API">MDN: Gamepad API</a> — suporte a controles no navegador</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Touch_events">MDN: Eventos de toque</a> — manipulação multitoque em dispositivos móveis</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Física para jogos web]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/game-physics-libraries</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/game-physics-libraries</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Compare mecanismos de física para jogos web: Rapier, Cannon-es, Ammo.js, Matter.js e Planck.js. Benchmarks de desempenho, exemplos de código e quando usar cada um.]]></description>
            <content:encoded><![CDATA[<h1>Física para jogos web</h1>
<p>A simulação de física dá vida aos jogos — objetos caindo, bolas quicando, ragdolls, veículos e ambientes destrutíveis. Este guia aborda as principais bibliotecas de física disponíveis para jogos web, com exemplos reais de código e uma avaliação sincera dos prós e contras.</p>
<h2>Visão geral</h2>
<table>
<thead>
<tr>
<th>Mecanismo</th>
<th style="text-align:center">Dimensão</th>
<th style="text-align:center">Desempenho</th>
<th style="text-align:center">Tamanho</th>
<th style="text-align:center">Dificuldade</th>
<th style="text-align:center">Corpos deformáveis</th>
<th style="text-align:center">Veículos</th>
<th style="text-align:center">CCD</th>
<th style="text-align:center">Determinismo</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Rapier</strong></td>
<td style="text-align:center">2D/3D</td>
<td style="text-align:center">⭐⭐⭐⭐⭐</td>
<td style="text-align:center">1,4 MB</td>
<td style="text-align:center">Média</td>
<td style="text-align:center">—</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
</tr>
<tr>
<td><strong>Cannon-es</strong></td>
<td style="text-align:center">3D</td>
<td style="text-align:center">⭐⭐⭐</td>
<td style="text-align:center">150 KB</td>
<td style="text-align:center">Fácil</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
</tr>
<tr>
<td><strong>Ammo.js</strong></td>
<td style="text-align:center">3D</td>
<td style="text-align:center">⭐⭐⭐⭐⭐</td>
<td style="text-align:center">1–2 MB</td>
<td style="text-align:center">Difícil</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">⚠️</td>
</tr>
<tr>
<td><strong>Jolt</strong></td>
<td style="text-align:center">3D</td>
<td style="text-align:center">⭐⭐⭐⭐⭐</td>
<td style="text-align:center">—</td>
<td style="text-align:center">Difícil</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
</tr>
<tr>
<td><strong>Oimo.js</strong></td>
<td style="text-align:center">3D</td>
<td style="text-align:center">⭐⭐⭐</td>
<td style="text-align:center">100 KB</td>
<td style="text-align:center">Fácil</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
</tr>
<tr>
<td><strong>Matter.js</strong></td>
<td style="text-align:center">2D</td>
<td style="text-align:center">⭐⭐⭐</td>
<td style="text-align:center">80 KB</td>
<td style="text-align:center">Fácil</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">⚠️</td>
</tr>
<tr>
<td><strong>Planck.js</strong></td>
<td style="text-align:center">2D</td>
<td style="text-align:center">⭐⭐⭐⭐</td>
<td style="text-align:center">120 KB</td>
<td style="text-align:center">Média</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">⚠️</td>
</tr>
<tr>
<td><strong>p2-es</strong></td>
<td style="text-align:center">2D</td>
<td style="text-align:center">⭐⭐⭐</td>
<td style="text-align:center">100 KB</td>
<td style="text-align:center">Média</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
</tr>
<tr>
<td><strong>Box2D WASM</strong></td>
<td style="text-align:center">2D</td>
<td style="text-align:center">⭐⭐⭐⭐⭐</td>
<td style="text-align:center">300 KB</td>
<td style="text-align:center">Difícil</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
</tr>
</tbody>
</table>
<p><strong>Legenda:</strong></p>
<ul>
<li>CCD = detecção contínua de colisão (impede que objetos rápidos atravessem paredes)</li>
<li>⚠️ Determinismo = possível com um passo de tempo fixo, mas não é garantido entre plataformas</li>
</ul>
<h3>Recomendações rápidas</h3>
<table>
<thead>
<tr>
<th>Sua situação</th>
<th>Melhor escolha</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jogo 3D para produção</td>
<td><strong>Rapier</strong> — melhor desempenho, API moderna e desenvolvimento ativo</td>
</tr>
<tr>
<td>Aprendizado/protótipo</td>
<td><strong>Cannon-es</strong> (3D) ou <strong>Matter.js</strong> (2D) — APIs simples e depuração fácil</td>
</tr>
<tr>
<td>Física de veículos</td>
<td><strong>Rapier</strong> ou <strong>Ammo.js</strong> — ambos têm controladores de veículos por ray cast</td>
</tr>
<tr>
<td>Corpos deformáveis, tecidos e cordas</td>
<td><strong>Ammo.js</strong> ou <strong>Jolt (JoltPhysics.js)</strong> — ambos oferecem suporte a tecidos e corpos deformáveis; o port WASM do Jolt recebe manutenção mais ativa</td>
</tr>
<tr>
<td>Jogo de plataforma 2D preciso</td>
<td><strong>Planck.js</strong> — algoritmos do Box2D, determinístico com passo de tempo fixo</td>
</tr>
<tr>
<td>Desempenho máximo em 2D</td>
<td><strong>Box2D WASM</strong> — velocidade nativa no navegador</td>
</tr>
<tr>
<td>Menor tamanho de bundle</td>
<td><strong>Oimo.js</strong> (3D) ou <strong>Matter.js</strong> (2D)</td>
</tr>
</tbody>
</table>
<hr>
<h2>Comparação detalhada</h2>
<h3>Mecanismos de física 3D</h3>
<table>
<thead>
<tr>
<th>Mecanismo</th>
<th>Linguagem</th>
<th>Tamanho</th>
<th>Desempenho</th>
<th>Melhor para</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Rapier</strong></td>
<td>Rust/WASM</td>
<td>~1,4 MB</td>
<td>Excelente</td>
<td>Jogos para produção, simulações complexas</td>
</tr>
<tr>
<td><strong>Cannon-es</strong></td>
<td>JavaScript</td>
<td>~150 KB</td>
<td>Bom</td>
<td>Protótipos, jogos simples</td>
</tr>
<tr>
<td><strong>Ammo.js</strong></td>
<td>C++/WASM</td>
<td>~1–2 MB</td>
<td>Excelente</td>
<td>Recursos AAA, corpos deformáveis, veículos</td>
</tr>
<tr>
<td><strong>Jolt</strong></td>
<td>C++/WASM</td>
<td>WASM</td>
<td>Excelente</td>
<td>Recursos AAA com manutenção ativa</td>
</tr>
<tr>
<td><strong>Oimo.js</strong></td>
<td>JavaScript</td>
<td>~100 KB</td>
<td>Bom</td>
<td>Jogos simples, protótipos rápidos</td>
</tr>
</tbody>
</table>
<h3>Mecanismos de física 2D</h3>
<table>
<thead>
<tr>
<th>Mecanismo</th>
<th>Linguagem</th>
<th>Tamanho</th>
<th>Desempenho</th>
<th>Melhor para</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Matter.js</strong></td>
<td>JavaScript</td>
<td>~80 KB</td>
<td>Bom</td>
<td>Jogos visuais, protótipos</td>
</tr>
<tr>
<td><strong>Planck.js</strong></td>
<td>JavaScript</td>
<td>~120 KB</td>
<td>Bom</td>
<td>Jogos de plataforma, física precisa</td>
</tr>
<tr>
<td><strong>p2-es</strong></td>
<td>JavaScript</td>
<td>~100 KB</td>
<td>Bom</td>
<td>Restrições, mecanismos</td>
</tr>
<tr>
<td><strong>Box2D WASM</strong></td>
<td>C++/WASM</td>
<td>~300 KB</td>
<td>Excelente</td>
<td>Grandes quantidades de corpos</td>
</tr>
</tbody>
</table>
<hr>
<h2>Mecanismos de física 3D</h2>
<h3>Rapier — A escolha moderna</h3>
<p>Rapier é um mecanismo de física em Rust com bindings para JavaScript/WASM. É a opção de maior desempenho para jogos web em 2025–2026, com melhorias de velocidade de 2 a 5 vezes em relação à sua versão de 2024.</p>
<p><strong>Instalação:</strong></p>
<pre><code class="language-bash">npm install @dimforge/rapier3d
# Or with SIMD (faster, requires modern browsers):
npm install @dimforge/rapier3d-simd
</code></pre>
<p><strong>Configuração básica:</strong></p>
<pre><code class="language-js">import RAPIER from '@dimforge/rapier3d'

// Initialize (async required for WASM)
await RAPIER.init()

// Create world with gravity
const gravity = { x: 0, y: -9.81, z: 0 }
const world = new RAPIER.World(gravity)

// Create ground (static body)
const groundDesc = RAPIER.RigidBodyDesc.fixed()
const groundBody = world.createRigidBody(groundDesc)
const groundCollider = RAPIER.ColliderDesc.cuboid(50, 0.1, 50)
world.createCollider(groundCollider, groundBody)

// Create falling box (dynamic body)
const boxDesc = RAPIER.RigidBodyDesc.dynamic()
  .setTranslation(0, 10, 0)
const boxBody = world.createRigidBody(boxDesc)
const boxCollider = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5)
  .setDensity(1.0)
  .setRestitution(0.5)
world.createCollider(boxCollider, boxBody)
</code></pre>
<p><strong>Integração com o loop do jogo:</strong></p>
<pre><code class="language-js">const FIXED_TIMESTEP = 1 / 60

function physicsStep() {
  world.step()
}

function gameLoop() {
  physicsStep()
  
  // Sync render objects with physics
  const position = boxBody.translation()
  const rotation = boxBody.rotation()
  
  // Update your Three.js/Babylon mesh
  mesh.position.set(position.x, position.y, position.z)
  mesh.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w)
  
  requestAnimationFrame(gameLoop)
}
</code></pre>
<p><strong>Detecção de colisões:</strong></p>
<pre><code class="language-js">// Event-based collision detection
world.contactPairsWith(boxCollider, (otherCollider) =&gt; {
  console.log('Box is touching:', otherCollider)
})

// Ray casting
const ray = new RAPIER.Ray({ x: 0, y: 10, z: 0 }, { x: 0, y: -1, z: 0 })
const hit = world.castRay(ray, 100, true)
if (hit) {
  const hitPoint = ray.pointAt(hit.timeOfImpact)
  console.log('Hit at:', hitPoint)
}
</code></pre>
<p><strong>Juntas:</strong></p>
<pre><code class="language-js">// Create a hinge joint (door)
const jointData = RAPIER.JointData.revolute(
  { x: 0, y: 0, z: 0 },  // Anchor on body1
  { x: -1, y: 0, z: 0 }, // Anchor on body2
  { x: 0, y: 1, z: 0 }   // Rotation axis
)
world.createImpulseJoint(jointData, body1, body2, true)
</code></pre>
<p><strong>Controlador de veículo:</strong></p>
<pre><code class="language-js">// Create chassis rigid body
const chassisDesc = RAPIER.RigidBodyDesc.dynamic()
  .setTranslation(0, 2, 0)
const chassis = world.createRigidBody(chassisDesc)
const chassisCollider = RAPIER.ColliderDesc.cuboid(1, 0.5, 2)
  .setDensity(100)
world.createCollider(chassisCollider, chassis)

// Create vehicle controller
const vehicle = world.createVehicleController(chassis)

// Add wheels (front-left, front-right, rear-left, rear-right)
const suspensionRestLength = 0.3
const wheelRadius = 0.4

// Front wheels (steering)
vehicle.addWheel(
  { x: -0.8, y: 0, z: 1.2 },   // Connection point
  { x: 0, y: -1, z: 0 },       // Suspension direction
  { x: -1, y: 0, z: 0 },       // Axle direction
  suspensionRestLength,
  wheelRadius
)
vehicle.addWheel(
  { x: 0.8, y: 0, z: 1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)

// Rear wheels (drive)
vehicle.addWheel(
  { x: -0.8, y: 0, z: -1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)
vehicle.addWheel(
  { x: 0.8, y: 0, z: -1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)

// Configure suspension for all wheels
for (let i = 0; i &lt; 4; i++) {
  vehicle.setWheelSuspensionStiffness(i, 30)
  vehicle.setWheelSuspensionCompression(i, 4.4)
  vehicle.setWheelSuspensionRelaxation(i, 2.3)
  vehicle.setWheelMaxSuspensionTravel(i, 0.5)
  vehicle.setWheelFrictionSlip(i, 2)
}

// In game loop
function updateVehicle(steering, engineForce, brakeForce) {
  // Steering (front wheels only)
  vehicle.setWheelSteering(0, steering)
  vehicle.setWheelSteering(1, steering)
  
  // Engine (rear wheels)
  vehicle.setWheelEngineForce(2, engineForce)
  vehicle.setWheelEngineForce(3, engineForce)
  
  // Brakes (all wheels)
  for (let i = 0; i &lt; 4; i++) {
    vehicle.setWheelBrake(i, brakeForce)
  }
  
  // Update vehicle physics
  vehicle.updateVehicle(world.timestep)
}
</code></pre>
<p><strong>Prós:</strong></p>
<ul>
<li>Melhor desempenho (WASM + SIMD)</li>
<li>Excelente precisão na detecção de colisões</li>
<li>Determinismo multiplataforma (mesmas entradas = mesmas saídas)</li>
<li>Desenvolvimento ativo e API moderna</li>
<li>Detecção contínua de colisão (sem atravessamento)</li>
<li>Controladores de personagem e de veículo integrados</li>
</ul>
<p><strong>Contras:</strong></p>
<ul>
<li>Bundle maior (~1,4 MB)</li>
<li>Requer inicialização assíncrona</li>
<li>API mais complexa do que as alternativas em JavaScript puro</li>
<li>A depuração de WASM pode ser complicada</li>
</ul>
<hr>
<h3>Cannon-es — Simples e eficaz</h3>
<p>Cannon-es é o fork mantido do Cannon.js. Escrito em JavaScript puro, é fácil de entender e excelente para aprendizado e protótipos.</p>
<p><strong>Instalação:</strong></p>
<pre><code class="language-bash">npm install cannon-es
</code></pre>
<p><strong>Configuração básica:</strong></p>
<pre><code class="language-js">import * as CANNON from 'cannon-es'

// Create world
const world = new CANNON.World({
  gravity: new CANNON.Vec3(0, -9.81, 0)
})

// Ground
const groundBody = new CANNON.Body({
  type: CANNON.Body.STATIC,
  shape: new CANNON.Plane()
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)

// Falling sphere
const sphereBody = new CANNON.Body({
  mass: 1,
  shape: new CANNON.Sphere(0.5),
  position: new CANNON.Vec3(0, 10, 0)
})
sphereBody.linearDamping = 0.1
world.addBody(sphereBody)
</code></pre>
<p><strong>Loop do jogo:</strong></p>
<pre><code class="language-js">const TIMESTEP = 1 / 60

function animate() {
  world.step(TIMESTEP)
  
  // Sync with Three.js
  mesh.position.copy(sphereBody.position)
  mesh.quaternion.copy(sphereBody.quaternion)
  
  requestAnimationFrame(animate)
}
</code></pre>
<p><strong>Eventos de colisão:</strong></p>
<pre><code class="language-js">sphereBody.addEventListener('collide', (event) =&gt; {
  const contact = event.contact
  const impactVelocity = contact.getImpactVelocityAlongNormal()
  
  if (Math.abs(impactVelocity) &gt; 5) {
    console.log('Hard impact!')
  }
})
</code></pre>
<p><strong>Restrições:</strong></p>
<pre><code class="language-js">// Distance constraint (rope-like)
const constraint = new CANNON.DistanceConstraint(
  bodyA, bodyB, 
  2 // distance
)
world.addConstraint(constraint)

// Hinge constraint
const hinge = new CANNON.HingeConstraint(bodyA, bodyB, {
  pivotA: new CANNON.Vec3(1, 0, 0),
  axisA: new CANNON.Vec3(0, 1, 0),
  pivotB: new CANNON.Vec3(-1, 0, 0),
  axisB: new CANNON.Vec3(0, 1, 0)
})
world.addConstraint(hinge)
</code></pre>
<p><strong>Prós:</strong></p>
<ul>
<li>JavaScript puro, sem a complexidade do WASM</li>
<li>Bundle pequeno (~150 KB)</li>
<li>Fácil de aprender e depurar</li>
<li>Funciona em qualquer lugar</li>
<li>Excelente integração com Three.js</li>
<li>Boa documentação</li>
</ul>
<p><strong>Contras:</strong></p>
<ul>
<li>Mais lento do que as alternativas em WASM</li>
<li>Tem dificuldades com muitos corpos (&gt;100)</li>
<li>Suporte limitado a malhas trianguladas</li>
<li>Sem CCD integrado (objetos podem atravessar superfícies)</li>
<li>Desenvolvimento menos ativo</li>
</ul>
<hr>
<h3>Ammo.js — Todo o poder do Bullet Physics</h3>
<p>Ammo.js é o mecanismo Bullet Physics compilado para WebAssembly. Oferece o conjunto máximo de recursos, incluindo corpos deformáveis, veículos e restrições avançadas.</p>
<p><strong>Instalação:</strong></p>
<pre><code class="language-bash">npm install ammo.js
# Or use from CDN
</code></pre>
<p><strong>Configuração básica:</strong></p>
<pre><code class="language-js">import Ammo from 'ammo.js'

let physicsWorld

async function initPhysics() {
  await Ammo()
  
  const collisionConfig = new Ammo.btDefaultCollisionConfiguration()
  const dispatcher = new Ammo.btCollisionDispatcher(collisionConfig)
  const broadphase = new Ammo.btDbvtBroadphase()
  const solver = new Ammo.btSequentialImpulseConstraintSolver()
  
  physicsWorld = new Ammo.btDiscreteDynamicsWorld(
    dispatcher, broadphase, solver, collisionConfig
  )
  physicsWorld.setGravity(new Ammo.btVector3(0, -9.81, 0))
}

function createBox(mass, width, height, depth, x, y, z) {
  const transform = new Ammo.btTransform()
  transform.setIdentity()
  transform.setOrigin(new Ammo.btVector3(x, y, z))
  
  const motionState = new Ammo.btDefaultMotionState(transform)
  const shape = new Ammo.btBoxShape(
    new Ammo.btVector3(width / 2, height / 2, depth / 2)
  )
  
  const localInertia = new Ammo.btVector3(0, 0, 0)
  if (mass &gt; 0) {
    shape.calculateLocalInertia(mass, localInertia)
  }
  
  const rbInfo = new Ammo.btRigidBodyConstructionInfo(
    mass, motionState, shape, localInertia
  )
  const body = new Ammo.btRigidBody(rbInfo)
  
  physicsWorld.addRigidBody(body)
  return body
}
</code></pre>
<p><strong>Corpos deformáveis (tecido):</strong></p>
<pre><code class="language-js">function createCloth(width, height, segments) {
  const softBodyHelpers = new Ammo.btSoftBodyHelpers()
  
  const corner00 = new Ammo.btVector3(-width/2, height, 0)
  const corner10 = new Ammo.btVector3(width/2, height, 0)
  const corner01 = new Ammo.btVector3(-width/2, 0, 0)
  const corner11 = new Ammo.btVector3(width/2, 0, 0)
  
  const softBody = softBodyHelpers.CreatePatch(
    physicsWorld.getWorldInfo(),
    corner00, corner10, corner01, corner11,
    segments, segments,
    0, true
  )
  
  const sbConfig = softBody.get_m_cfg()
  sbConfig.set_viterations(10)
  sbConfig.set_piterations(10)
  
  softBody.setTotalMass(0.9, false)
  Ammo.castObject(softBody, Ammo.btCollisionObject)
    .getCollisionShape().setMargin(0.05)
  
  physicsWorld.addSoftBody(softBody, 1, -1)
  return softBody
}
</code></pre>
<p><strong>Física de veículos:</strong></p>
<pre><code class="language-js">function createVehicle(chassisBody) {
  const tuning = new Ammo.btVehicleTuning()
  const rayCaster = new Ammo.btDefaultVehicleRaycaster(physicsWorld)
  const vehicle = new Ammo.btRaycastVehicle(tuning, chassisBody, rayCaster)
  
  vehicle.setCoordinateSystem(0, 1, 2)
  physicsWorld.addAction(vehicle)
  
  // Add wheels
  const wheelRadius = 0.4
  const wheelWidth = 0.3
  const suspensionRestLength = 0.3
  
  const wheelDirectionCS = new Ammo.btVector3(0, -1, 0)
  const wheelAxleCS = new Ammo.btVector3(-1, 0, 0)
  
  function addWheel(isFront, pos) {
    const wheelInfo = vehicle.addWheel(
      pos, wheelDirectionCS, wheelAxleCS,
      suspensionRestLength, wheelRadius, tuning, isFront
    )
    wheelInfo.set_m_suspensionStiffness(20)
    wheelInfo.set_m_wheelsDampingRelaxation(2.3)
    wheelInfo.set_m_wheelsDampingCompression(4.4)
    wheelInfo.set_m_frictionSlip(1000)
    wheelInfo.set_m_rollInfluence(0.1)
  }
  
  addWheel(true, new Ammo.btVector3(1, 0, 1.5))   // Front left
  addWheel(true, new Ammo.btVector3(-1, 0, 1.5))  // Front right
  addWheel(false, new Ammo.btVector3(1, 0, -1.5)) // Rear left
  addWheel(false, new Ammo.btVector3(-1, 0, -1.5))// Rear right
  
  return vehicle
}
</code></pre>
<p><strong>Prós:</strong></p>
<ul>
<li>Conjunto de recursos mais completo</li>
<li>Corpos deformáveis, tecidos e cordas</li>
<li>Física avançada de veículos</li>
<li>Amplamente testado em produção (usado em muitos jogos AAA)</li>
<li>Altamente configurável
<strong>Contras:</strong></li>
<li>API complexa e verbosa</li>
<li>Tamanho de bundle grande</li>
<li>Exige gerenciamento de memória (destruir objetos)</li>
<li>Curva de aprendizado acentuada</li>
<li>Documentação dispersa</li>
<li>Não é determinístico entre plataformas (apenas no mesmo dispositivo e com configuração cuidadosa)</li>
</ul>
<hr>
<h3>Jolt — física 3D moderna de nível AAA</h3>
<p>O Jolt Physics é o motor por trás de jogos como Horizon Forbidden West, e o JoltPhysics.js o leva ao navegador como um port para WASM. Ele é um excelente meio-termo entre Rapier e Ammo.js: oferece mais recursos que o Rapier (corpos macios, tecidos e um controlador de veículos com rodas), além da manutenção ativa que falta ao Ammo.js. O pacote npm é <code>jolt-physics</code>, e há integrações prontas para React Three Fiber (<code>@react-three/jolt</code>) e Babylon.js.</p>
<pre><code class="language-bash">npm install jolt-physics
</code></pre>
<p>Assim como o Rapier, ele usa WASM com inicialização assíncrona e tem uma API verbosa, porém completa, que espelha a interface em C++. Escolha o Jolt quando quiser recursos do nível do Bullet (corpos macios e veículos), mas preferir uma base de código moderna e mantida em vez do Ammo.js.</p>
<hr>
<h3>Oimo.js — leve e rápido</h3>
<p>O Oimo.js é um motor de física 3D leve. É excelente para jogos simples que não precisam de recursos avançados.</p>
<p><strong>Instalação:</strong></p>
<pre><code class="language-bash">npm install oimo
</code></pre>
<p><strong>Configuração básica:</strong></p>
<pre><code class="language-js">import * as OIMO from 'oimo'

const world = new OIMO.World({
  timestep: 1/60,
  iterations: 8,
  broadphase: 2, // 1: força bruta, 2: varredura e poda, 3: árvore de volumes
  worldscale: 1,
  random: true,
  gravity: [0, -9.8, 0]
})

// Criar o chão
world.add({
  type: 'box',
  size: [100, 1, 100],
  pos: [0, -0.5, 0],
  move: false
})

// Criar uma esfera em queda
const sphere = world.add({
  type: 'sphere',
  size: [1],
  pos: [0, 10, 0],
  move: true,
  density: 1,
  friction: 0.4,
  restitution: 0.2
})
</code></pre>
<p><strong>Loop do jogo:</strong></p>
<pre><code class="language-js">function animate() {
  world.step()
  
  // Obter posição/rotação
  const pos = sphere.getPosition()
  const rot = sphere.getQuaternion()
  
  mesh.position.set(pos.x, pos.y, pos.z)
  mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w)
  
  requestAnimationFrame(animate)
}
</code></pre>
<p><strong>Prós:</strong></p>
<ul>
<li>Muito pequeno (~100 KB)</li>
<li>API simples</li>
<li>Bom desempenho em cenas básicas</li>
<li>Suporte integrado ao Babylon.js</li>
</ul>
<p><strong>Contras:</strong></p>
<ul>
<li>Formas limitadas (apenas primitivas)</li>
<li>Sem corpos macios</li>
<li>Documentação escassa</li>
<li>Desenvolvimento menos ativo</li>
<li>Opções limitadas de juntas</li>
</ul>
<hr>
<h2>Motores de física 2D</h2>
<h3>Matter.js — bonito e intuitivo</h3>
<p>O Matter.js é um motor de física 2D repleto de recursos, com excelentes ferramentas de renderização e depuração.</p>
<p><strong>Instalação:</strong></p>
<pre><code class="language-bash">npm install matter-js
</code></pre>
<p><strong>Configuração básica:</strong></p>
<pre><code class="language-js">import Matter from 'matter-js'

const { Engine, Render, World, Bodies, Runner } = Matter

// Criar o motor
const engine = Engine.create()

// Criar o renderizador (opcional, ótimo para depuração)
const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: 800,
    height: 600,
    wireframes: false
  }
})

// Criar corpos
const ground = Bodies.rectangle(400, 580, 810, 60, { 
  isStatic: true 
})

const box = Bodies.rectangle(400, 200, 80, 80, {
  restitution: 0.8,
  friction: 0.5
})

const circle = Bodies.circle(300, 100, 40, {
  restitution: 0.9
})

// Adicionar ao mundo
World.add(engine.world, [ground, box, circle])

// Executar
Render.run(render)
Runner.run(Runner.create(), engine)
</code></pre>
<p><strong>Eventos de colisão:</strong></p>
<pre><code class="language-js">Matter.Events.on(engine, 'collisionStart', (event) =&gt; {
  event.pairs.forEach(pair =&gt; {
    console.log('Colisão entre:', pair.bodyA.label, pair.bodyB.label)
  })
})
</code></pre>
<p><strong>Restrições:</strong></p>
<pre><code class="language-js">// Restrição de pino
const pin = Matter.Constraint.create({
  pointA: { x: 400, y: 100 },
  bodyB: box,
  stiffness: 0.9
})

// Mola entre corpos
const spring = Matter.Constraint.create({
  bodyA: box,
  bodyB: circle,
  stiffness: 0.01,
  length: 100
})

World.add(engine.world, [pin, spring])
</code></pre>
<p><strong>Interação com o mouse:</strong></p>
<pre><code class="language-js">const mouse = Matter.Mouse.create(render.canvas)
const mouseConstraint = Matter.MouseConstraint.create(engine, {
  mouse: mouse,
  constraint: {
    stiffness: 0.2,
    render: { visible: false }
  }
})
World.add(engine.world, mouseConstraint)
</code></pre>
<p><strong>Prós:</strong></p>
<ul>
<li>Bela renderização padrão</li>
<li>Ótimo para aprender e criar protótipos</li>
<li>API intuitiva</li>
<li>Boa documentação</li>
<li>Comunidade ativa</li>
<li>Determinístico por padrão — o Matter.Runner agora usa um passo de tempo fixo e determinístico (o passo de tempo não fixo foi removido na v0.20.0)</li>
</ul>
<p><strong>Contras:</strong></p>
<ul>
<li>Sem CCD (objetos rápidos podem atravessar outros — use subpassos para atenuar o problema)</li>
<li>Problemas de desempenho com muitos corpos</li>
<li>Sem opção em WASM</li>
<li>Precisão limitada em simulações complexas</li>
</ul>
<hr>
<h3>Planck.js — Box2D para JavaScript</h3>
<p>O Planck.js é uma reescrita completa do Box2D em JavaScript. Oferece física testada em produção e é determinístico ao usar um passo de tempo fixo.</p>
<p><strong>Instalação:</strong></p>
<pre><code class="language-bash">npm install planck
</code></pre>
<p><strong>Configuração básica:</strong></p>
<pre><code class="language-js">import { World, Vec2, Box, Circle, Edge } from 'planck'

// Criar o mundo
const world = new World({
  gravity: Vec2(0, -10)
})

// Criar o chão
const ground = world.createBody()
ground.createFixture({
  shape: Edge(Vec2(-40, 0), Vec2(40, 0))
})

// Criar uma caixa dinâmica
const box = world.createBody({
  type: 'dynamic',
  position: Vec2(0, 10)
})
box.createFixture({
  shape: Box(1, 1),
  density: 1,
  friction: 0.3,
  restitution: 0.5
})
</code></pre>
<p><strong>Loop do jogo com passo de tempo fixo:</strong></p>
<pre><code class="language-js">const TIMESTEP = 1 / 60
const VELOCITY_ITERATIONS = 8
const POSITION_ITERATIONS = 3

function gameLoop() {
  world.step(TIMESTEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS)
  
  // Percorrer todos os corpos
  for (let body = world.getBodyList(); body; body = body.getNext()) {
    const pos = body.getPosition()
    const angle = body.getAngle()
    // Atualize seus sprites...
  }
  
  requestAnimationFrame(gameLoop)
}
</code></pre>
<p><strong>Callbacks de colisão:</strong></p>
<pre><code class="language-js">world.on('begin-contact', (contact) =&gt; {
  const fixtureA = contact.getFixtureA()
  const fixtureB = contact.getFixtureB()
  console.log('Contato iniciado')
})

world.on('end-contact', (contact) =&gt; {
  console.log('Contato encerrado')
})

world.on('pre-solve', (contact, oldManifold) =&gt; {
  // É possível desativar o contato aqui
  // contact.setEnabled(false)
})
</code></pre>
<p><strong>Juntas:</strong></p>
<pre><code class="language-js">import { RevoluteJoint, DistanceJoint, PrismaticJoint } from 'planck'

// Junta de revolução (dobradiça)
const joint = world.createJoint(RevoluteJoint({
  bodyA: ground,
  bodyB: box,
  localAnchorA: Vec2(0, 5),
  localAnchorB: Vec2(-1, 0),
  enableMotor: true,
  maxMotorTorque: 1000,
  motorSpeed: 2
}))

// Junta de distância (mola)
world.createJoint(DistanceJoint({
  bodyA: boxA,
  bodyB: boxB,
  localAnchorA: Vec2(0, 0),
  localAnchorB: Vec2(0, 0),
  length: 5,
  stiffness: 10,
  damping: 0.5
}))
</code></pre>
<p><strong>Prós:</strong></p>
<ul>
<li>Determinístico com passo de tempo fixo</li>
<li>Bem documentado (a documentação do Box2D também se aplica)</li>
<li>Bom desempenho</li>
<li>Suporte a TypeScript</li>
<li>Bundle pequeno</li>
</ul>
<p><strong>Contras:</strong></p>
<ul>
<li>O CCD não funciona com juntas, portanto objetos rápidos conectados por juntas podem se esticar</li>
<li>Curva de aprendizado mais acentuada que a do Matter.js</li>
<li>Sem renderizador integrado</li>
<li>Peculiaridades do Box2D (a escala das unidades importa)</li>
</ul>
<hr>
<h3>p2-es — física 2D flexível</h3>
<p>O p2-es é o fork mantido do p2.js. É excelente para jogos que precisam de restrições e mecanismos complexos.</p>
<p><strong>Instalação:</strong></p>
<pre><code class="language-bash">npm install p2-es
</code></pre>
<p><strong>Configuração básica:</strong></p>
<pre><code class="language-js">import * as p2 from 'p2-es'

const world = new p2.World({
  gravity: [0, -9.81]
})

// Chão
const groundBody = new p2.Body({
  mass: 0, // estático
  position: [0, -1]
})
groundBody.addShape(new p2.Plane())
world.addBody(groundBody)

// Círculo dinâmico
const circleBody = new p2.Body({
  mass: 1,
  position: [0, 5]
})
circleBody.addShape(new p2.Circle({ radius: 0.5 }))
world.addBody(circleBody)
</code></pre>
<p><strong>Restrições avançadas:</strong></p>
<pre><code class="language-js">// Restrição de engrenagem
const gear = new p2.GearConstraint(bodyA, bodyB, {
  ratio: 2 // bodyB gira duas vezes mais rápido
})
world.addConstraint(gear)

// Restrição prismática (deslizante)
const prismatic = new p2.PrismaticConstraint(bodyA, bodyB, {
  localAnchorA: [0, 0],
  localAnchorB: [0, 0],
  localAxisA: [1, 0],
  disableRotationalLock: false
})
world.addConstraint(prismatic)

// Restrição de bloqueio (solda)
const lock = new p2.LockConstraint(bodyA, bodyB)
world.addConstraint(lock)
</code></pre>
<p><strong>Materiais de contato:</strong></p>
<pre><code class="language-js">const ice = new p2.Material()
const rubber = new p2.Material()

const iceRubber = new p2.ContactMaterial(ice, rubber, {
  friction: 0.1,
  restitution: 0.9
})
world.addContactMaterial(iceRubber)

// Aplicar às formas
iceBody.shapes[0].material = ice
rubberBody.shapes[0].material = rubber
</code></pre>
<p><strong>Prós:</strong></p>
<ul>
<li>Sistema de restrições completo</li>
<li>Materiais de contato</li>
<li>Bom para mecanismos e máquinas</li>
<li>Corpos em repouso (melhora o desempenho)</li>
<li>Módulos ES e suporte a tree shaking</li>
</ul>
<p><strong>Contras:</strong></p>
<ul>
<li>Sem renderizador integrado</li>
<li>Alguns pares de formas não são compatíveis</li>
<li>Menos ativo que as alternativas</li>
<li>Lacunas na documentação</li>
</ul>
<hr>
<h2>Dicas de desempenho</h2>
<h3>1. Use um passo de tempo fixo</h3>
<pre><code class="language-js">const TIMESTEP = 1 / 60
let accumulator = 0

function gameLoop(deltaTime) {
  accumulator += deltaTime
  
  while (accumulator &gt;= TIMESTEP) {
    world.step(TIMESTEP)
    accumulator -= TIMESTEP
  }
  
  // Interpolar para obter uma renderização suave
  const alpha = accumulator / TIMESTEP
  // lerp(previousState, currentState, alpha)
}
</code></pre>
<h3>2. Coloque corpos inativos em repouso</h3>
<p>A maioria dos motores oferece suporte ao repouso. Ative-o:</p>
<pre><code class="language-js">// Cannon-es
world.allowSleep = true
body.allowSleep = true
body.sleepSpeedLimit = 0.1
body.sleepTimeLimit = 1

// Rapier
bodyDesc.setCanSleep(true)
</code></pre>
<h3>3. Use formas de colisão simples</h3>
<pre><code class="language-js">// BOM: primitivas simples
const sphere = new CANNON.Sphere(1)
const box = new CANNON.Box(new CANNON.Vec3(1, 1, 1))

// EVITE: trimesh complexo para corpos dinâmicos
const trimesh = new CANNON.Trimesh(vertices, indices) // Lento!
</code></pre>
<h3>4. Reduza as iterações do solver (com cuidado)</h3>
<pre><code class="language-js">// Cannon-es
world.solver.iterations = 5 // Padrão: 10

// Rapier - configurado na criação do mundo
</code></pre>
<h3>5. Execute a física em um Web Worker</h3>
<pre><code class="language-js">// main.js
const worker = new Worker('physics-worker.js')

worker.postMessage({ type: 'step', deltaTime: 1/60 })
worker.onmessage = (e) =&gt; {
  const { positions, rotations } = e.data
  // Atualizar objetos de renderização
}

// physics-worker.js
importScripts('cannon-es.js')
const world = new CANNON.World()

onmessage = (e) =&gt; {
  if (e.data.type === 'step') {
    world.step(e.data.deltaTime)
    postMessage({
      positions: bodies.map(b =&gt; b.position.toArray()),
      rotations: bodies.map(b =&gt; b.quaternion.toArray())
    })
  }
}
</code></pre>
<hr>
<h2>Quando usar cada opção</h2>
<table>
<thead>
<tr>
<th>Cenário</th>
<th>Recomendação</th>
<th>Motivo</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jogo 3D para produção</td>
<td><strong>Rapier</strong></td>
<td>Melhor desempenho, API moderna e veículos</td>
</tr>
<tr>
<td>Protótipo de jogo 3D</td>
<td><strong>Cannon-es</strong></td>
<td>Simples e fácil de depurar</td>
</tr>
<tr>
<td>Corpos macios, tecidos e cordas</td>
<td><strong>Ammo.js</strong> ou <strong>Jolt</strong></td>
<td>Ambos oferecem suporte a tecidos e corpos macios; o port para WASM do Jolt (JoltPhysics.js) recebe manutenção mais ativa</td>
</tr>
<tr>
<td>Jogo 3D simples para navegador</td>
<td><strong>Oimo.js</strong></td>
<td>Minúsculo e suficiente</td>
</tr>
<tr>
<td>Game jam de jogos 2D</td>
<td><strong>Matter.js</strong></td>
<td>Configuração rápida e renderização integrada</td>
</tr>
<tr>
<td>Jogo de plataforma preciso</td>
<td><strong>Planck.js</strong></td>
<td>Determinismo com passo de tempo fixo e boa documentação</td>
</tr>
<tr>
<td>Mecanismos 2D complexos</td>
<td><strong>p2-es</strong></td>
<td>Sistema de restrições completo</td>
</tr>
<tr>
<td>Jogo 2D crítico em desempenho</td>
<td><strong>Box2D WASM</strong></td>
<td>Opção 2D mais rápida e CCD verdadeiro</td>
</tr>
</tbody>
</table>
<hr>
<h2>Integração com renderizadores</h2>
<h3>Three.js + Rapier</h3>
<pre><code class="language-js">import * as THREE from 'three'
import RAPIER from '@dimforge/rapier3d'

await RAPIER.init()

const scene = new THREE.Scene()
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 })

const bodies = new Map() // corpo físico -&gt; malha

function createPhysicsBox(x, y, z) {
  // Física
  const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y, z)
  const body = world.createRigidBody(bodyDesc)
  const collider = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5)
  world.createCollider(collider, body)
  
  // Renderização
  const mesh = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0x00ff00 })
  )
  scene.add(mesh)
  
  bodies.set(body, mesh)
  return body
}

function syncPhysics() {
  bodies.forEach((mesh, body) =&gt; {
    const pos = body.translation()
    const rot = body.rotation()
    mesh.position.set(pos.x, pos.y, pos.z)
    mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w)
  })
}
</code></pre>
<h3>PixiJS + Matter.js</h3>
<pre><code class="language-js">import * as PIXI from 'pixi.js'
import Matter from 'matter-js'

const app = new PIXI.Application({ width: 800, height: 600 })
document.body.appendChild(app.view)

const engine = Matter.Engine.create()
const bodies = new Map()

function createPhysicsSprite(texture, x, y, width, height) {
  // Física
  const body = Matter.Bodies.rectangle(x, y, width, height)
  Matter.World.add(engine.world, body)
  
  // Renderização
  const sprite = new PIXI.Sprite(texture)
  sprite.anchor.set(0.5)
  app.stage.addChild(sprite)
  
  bodies.set(body, sprite)
  return body
}

app.ticker.add(() =&gt; {
  Matter.Engine.update(engine, 1000 / 60)
  
  bodies.forEach((sprite, body) =&gt; {
    sprite.position.set(body.position.x, body.position.y)
    sprite.rotation = body.angle
  })
})
</code></pre>
<hr>
<h2>Armadilhas comuns</h2>
<h3>A escala importa</h3>
<p>Os motores de física funcionam melhor com uma escala do mundo real (1 unidade = 1 metro). Não use coordenadas em pixels diretamente.</p>
<pre><code class="language-js">// RUIM: usar posições em pixels
const body = Bodies.circle(400, 300, 50) // Raio de 50 pixels?

// BOM: usar um fator de escala
const SCALE = 50 // 50 pixels por metro
const body = Bodies.circle(8, 6, 1) // Raio de 1 metro
// Depois, multiplicar por SCALE ao renderizar
</code></pre>
<h3>Vazamentos de memória com motores WASM</h3>
<p>Sempre faça a limpeza dos corpos ao removê-los:</p>
<pre><code class="language-js">// Rapier
world.removeRigidBody(body)

// Ammo.js
physicsWorld.removeRigidBody(body)
Ammo.destroy(body)
Ammo.destroy(shape)
Ammo.destroy(motionState)
</code></pre>
<h3>Tunelamento (objetos atravessando uns aos outros)</h3>
<p>Objetos em movimento rápido podem atravessar paredes finas. Soluções:</p>
<pre><code class="language-js">// Rapier: ativar CCD
const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true)

// Cannon-es: usar passos de tempo menores ou paredes mais grossas
world.step(1/120) // 120 Hz em vez de 60
</code></pre>
<hr>
<h2>Relacionados</h2>
<ul>
<li><a href="/pt-BR/tutorials/webgl-fundamentals-for-games">Fundamentos de WebGL para desenvolvedores de jogos</a></li>
<li><a href="/pt-BR/tutorials/web-workers-game-logic">Web Workers para lógica de jogos</a> — transfira a física para uma thread de Worker</li>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rápido</a></li>
<li><a href="/pt-BR/tutorials/canvas-2d-game-loop">Loop de jogo com Canvas 2D</a> — o padrão de passo de tempo fixo usado por todos os motores de física</li>
<li><a href="/pt-BR/guides/web-games-stack-2026">Stack tecnológico de jogos web em 2026</a> — como os motores de física se encaixam no stack mais amplo</li>
<li><a href="/pt-BR/guides/web-game-engines-comparison">Comparação de motores de jogos web</a> — motores com suporte integrado a física</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://rapier.rs/docs/">Documentação do Rapier</a> — documentação oficial e exemplos de física do Rapier</li>
<li><a href="https://pmndrs.github.io/cannon-es/docs/">Documentação do Cannon-es</a> — referência da API e guias</li>
<li><a href="https://brm.io/matter-js/docs/">Documentação do Matter.js</a> — referência completa da API</li>
<li><a href="https://piqnt.com/planck.js/">Documentação do Planck.js</a> — Box2D para JavaScript</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Gamepad API para jogos web]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/gamepad-api</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/gamepad-api</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Suporte completo a gamepads: detecção, mapeamento de botões, controles analógicos, vibração e uso de vários controles. Entrada com qualidade de console na web.]]></description>
            <content:encoded><![CDATA[<h1>Gamepad API para jogos web</h1>
<p>A Gamepad API oferece suporte a controles com qualidade de console em jogos web. Compatível com controles de Xbox, PlayStation, Switch Pro e modelos genéricos.</p>
<h2>1) Detectando gamepads</h2>
<pre><code class="language-js">window.addEventListener('gamepadconnected', (e) =&gt; {
  console.log('Gamepad conectado:', e.gamepad.id)
  console.log('Índice:', e.gamepad.index)
  console.log('Botões:', e.gamepad.buttons.length)
  console.log('Eixos:', e.gamepad.axes.length)
})

window.addEventListener('gamepaddisconnected', (e) =&gt; {
  console.log('Gamepad desconectado:', e.gamepad.id)
})
</code></pre>
<h2>2) Consultando o estado do gamepad</h2>
<p>O estado do gamepad deve ser consultado periodicamente (em vez de ser orientado a eventos):</p>
<pre><code class="language-js">function pollGamepads() {
  const gamepads = navigator.getGamepads()
  
  for (const gamepad of gamepads) {
    if (gamepad) {
      processGamepad(gamepad)
    }
  }
}

// Chame no loop do jogo
function gameLoop() {
  pollGamepads()
  update()
  render()
  requestAnimationFrame(gameLoop)
}
</code></pre>
<h2>3) Mapeamento padrão de botões</h2>
<p>O mapeamento <code>&quot;standard&quot;</code> (usado pela maioria dos controles de Xbox e PlayStation):</p>
<pre><code class="language-js">const BUTTONS = {
  A: 0,           // X (PS) / A (Xbox)
  B: 1,           // Círculo (PS) / B (Xbox)
  X: 2,           // Quadrado (PS) / X (Xbox)
  Y: 3,           // Triângulo (PS) / Y (Xbox)
  LB: 4,          // L1 (PS) / LB (Xbox)
  RB: 5,          // R1 (PS) / RB (Xbox)
  LT: 6,          // L2 (PS) / LT (Xbox)
  RT: 7,          // R2 (PS) / RT (Xbox)
  SELECT: 8,      // Share (PS) / Back (Xbox)
  START: 9,       // Options (PS) / Start (Xbox)
  L3: 10,         // Pressionar o analógico esquerdo
  R3: 11,         // Pressionar o analógico direito
  DPAD_UP: 12,
  DPAD_DOWN: 13,
  DPAD_LEFT: 14,
  DPAD_RIGHT: 15,
  HOME: 16,       // Botão PS / botão Xbox
}

const AXES = {
  LEFT_X: 0,
  LEFT_Y: 1,
  RIGHT_X: 2,
  RIGHT_Y: 3,
}
</code></pre>
<h2>4) Lendo botões e controles analógicos</h2>
<pre><code class="language-js">function processGamepad(gamepad) {
  // Botões (digitais ou analógicos)
  const jump = gamepad.buttons[BUTTONS.A].pressed
  const shoot = gamepad.buttons[BUTTONS.X].pressed
  
  // Gatilhos (analógicos, de 0 a 1)
  const leftTrigger = gamepad.buttons[BUTTONS.LT].value
  const rightTrigger = gamepad.buttons[BUTTONS.RT].value
  
  // Controles analógicos (de -1 a 1)
  const leftX = gamepad.axes[AXES.LEFT_X]
  const leftY = gamepad.axes[AXES.LEFT_Y]
  const rightX = gamepad.axes[AXES.RIGHT_X]
  const rightY = gamepad.axes[AXES.RIGHT_Y]
  
  return { jump, shoot, leftTrigger, rightTrigger, leftX, leftY, rightX, rightY }
}
</code></pre>
<h2>5) Tratamento da zona morta</h2>
<p>Os controles analógicos raramente ficam exatamente em 0 quando estão em repouso:</p>
<pre><code class="language-js">function applyDeadzone(value, deadzone = 0.15) {
  if (Math.abs(value) &lt; deadzone) return 0
  
  // Redimensiona o intervalo restante para 0-1
  const sign = Math.sign(value)
  const adjusted = (Math.abs(value) - deadzone) / (1 - deadzone)
  return sign * adjusted
}

function getStickInput(gamepad) {
  return {
    leftX: applyDeadzone(gamepad.axes[AXES.LEFT_X]),
    leftY: applyDeadzone(gamepad.axes[AXES.LEFT_Y]),
    rightX: applyDeadzone(gamepad.axes[AXES.RIGHT_X]),
    rightY: applyDeadzone(gamepad.axes[AXES.RIGHT_Y]),
  }
}
</code></pre>
<h2>6) Zona morta radial (melhor para movimentação 2D)</h2>
<pre><code class="language-js">function applyRadialDeadzone(x, y, deadzone = 0.15) {
  const magnitude = Math.sqrt(x * x + y * y)
  
  if (magnitude &lt; deadzone) {
    return { x: 0, y: 0 }
  }
  
  const normalized = {
    x: x / magnitude,
    y: y / magnitude,
  }
  
  const adjusted = (magnitude - deadzone) / (1 - deadzone)
  
  return {
    x: normalized.x * adjusted,
    y: normalized.y * adjusted,
  }
}

// Uso
const stick = applyRadialDeadzone(
  gamepad.axes[AXES.LEFT_X],
  gamepad.axes[AXES.LEFT_Y]
)
player.vx = stick.x * player.speed
player.vy = stick.y * player.speed
</code></pre>
<h2>7) Gerenciador completo de gamepads</h2>
<pre><code class="language-js">class GamepadManager {
  constructor() {
    this.gamepads = new Map()
    this.prevState = new Map()
    
    window.addEventListener('gamepadconnected', (e) =&gt; {
      this.gamepads.set(e.gamepad.index, e.gamepad)
      console.log('Conectado:', e.gamepad.id)
    })
    
    window.addEventListener('gamepaddisconnected', (e) =&gt; {
      this.gamepads.delete(e.gamepad.index)
      this.prevState.delete(e.gamepad.index)
    })
  }
  
  poll() {
    const gamepads = navigator.getGamepads()
    for (const gp of gamepads) {
      if (gp) this.gamepads.set(gp.index, gp)
    }
  }
  
  getState(index = 0) {
    const gp = this.gamepads.get(index)
    if (!gp) return null
    
    const stick = applyRadialDeadzone(gp.axes[0], gp.axes[1])
    const rightStick = applyRadialDeadzone(gp.axes[2], gp.axes[3])
    
    return {
      connected: true,
      leftStick: stick,
      rightStick: rightStick,
      
      // Botões frontais
      a: gp.buttons[0].pressed,
      b: gp.buttons[1].pressed,
      x: gp.buttons[2].pressed,
      y: gp.buttons[3].pressed,
      
      // Botões superiores
      lb: gp.buttons[4].pressed,
      rb: gp.buttons[5].pressed,
      
      // Gatilhos (analógicos)
      lt: gp.buttons[6].value,
      rt: gp.buttons[7].value,
      
      // Direcional
      dpadUp: gp.buttons[12].pressed,
      dpadDown: gp.buttons[13].pressed,
      dpadLeft: gp.buttons[14].pressed,
      dpadRight: gp.buttons[15].pressed,
      
      // Meta
      select: gp.buttons[8].pressed,
      start: gp.buttons[9].pressed,
    }
  }
  
  isPressed(index, button) {
    const gp = this.gamepads.get(index)
    return gp?.buttons[button]?.pressed || false
  }
  
  wasJustPressed(index, button) {
    const current = this.isPressed(index, button)
    const prev = this.prevState.get(index)?.[button] || false
    return current &amp;&amp; !prev
  }
  
  endFrame() {
    for (const [index, gp] of this.gamepads) {
      const state = {}
      for (let i = 0; i &lt; gp.buttons.length; i++) {
        state[i] = gp.buttons[i].pressed
      }
      this.prevState.set(index, state)
    }
  }
}

// Uso
const gamepadManager = new GamepadManager()

function update() {
  gamepadManager.poll()
  
  const state = gamepadManager.getState(0)
  if (state) {
    player.vx = state.leftStick.x * player.speed
    player.vy = state.leftStick.y * player.speed
    
    if (gamepadManager.wasJustPressed(0, BUTTONS.A)) {
      player.jump()
    }
  }
  
  gamepadManager.endFrame()
}
</code></pre>
<h2>8) Vibração / resposta tátil</h2>
<pre><code class="language-js">function vibrate(gamepad, duration = 200, weakMagnitude = 0.5, strongMagnitude = 0.5) {
  if (gamepad.vibrationActuator) {
    gamepad.vibrationActuator.playEffect('dual-rumble', {
      startDelay: 0,
      duration,
      weakMagnitude,   // Motor de alta frequência
      strongMagnitude, // Motor de baixa frequência
    })
  }
}

// Uso
function onPlayerHit() {
  const gp = navigator.getGamepads()[0]
  if (gp) vibrate(gp, 150, 0.3, 0.6)
}

function onExplosion() {
  const gp = navigator.getGamepads()[0]
  if (gp) vibrate(gp, 300, 0.8, 1.0)
}
</code></pre>
<h2>9) Vários jogadores</h2>
<pre><code class="language-js">class LocalMultiplayer {
  constructor(maxPlayers = 4) {
    this.maxPlayers = maxPlayers
    this.players = []
  }
  
  getActivePlayers() {
    const gamepads = navigator.getGamepads()
    const active = []
    
    for (let i = 0; i &lt; this.maxPlayers; i++) {
      if (gamepads[i]) {
        active.push({
          index: i,
          gamepad: gamepads[i],
          state: this.getState(gamepads[i])
        })
      }
    }
    
    return active
  }
  
  getState(gp) {
    // ... igual ao anterior
  }
}

// No loop do jogo
const multiplayer = new LocalMultiplayer(4)

function update() {
  const players = multiplayer.getActivePlayers()
  
  for (const { index, state } of players) {
    updatePlayer(index, state)
  }
}
</code></pre>
<h2>10) Alternativa e entrada combinada</h2>
<p>Ofereça suporte tanto ao teclado quanto ao gamepad:</p>
<pre><code class="language-js">class InputManager {
  constructor() {
    this.keyboard = new KeyboardInput()
    this.gamepad = new GamepadManager()
  }
  
  poll() {
    this.gamepad.poll()
  }
  
  getInput(playerIndex = 0) {
    const gpState = this.gamepad.getState(playerIndex)
    const kbState = this.keyboard.getState()
    
    // Combina as entradas (o gamepad tem prioridade para valores analógicos)
    return {
      moveX: gpState?.leftStick.x || (kbState.left ? -1 : kbState.right ? 1 : 0),
      moveY: gpState?.leftStick.y || (kbState.up ? -1 : kbState.down ? 1 : 0),
      jump: gpState?.a || kbState.space,
      shoot: gpState?.x || kbState.z,
      pause: gpState?.start || kbState.escape,
    }
  }
  
  endFrame() {
    this.gamepad.endFrame()
    this.keyboard.endFrame()
  }
}
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/game-input-handling">Tratamento de entradas em jogos</a></li>
<li><a href="/pt-BR/tutorials/mobile-friendly-web-games">Jogos web compatíveis com dispositivos móveis</a></li>
<li><a href="/pt-BR/tutorials/canvas-2d-game-loop">Loop de jogo com Canvas 2D</a></li>
<li><a href="/pt-BR/tutorials/pointer-lock-fps">Pointer Lock para jogos FPS</a> — combinando o gamepad com o controle de câmera pelo mouse</li>
<li><a href="/pt-BR/guides/co-op-game-design">Design de jogos cooperativos</a> — projetando para entradas de vários controles no modo cooperativo local</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API">MDN: Gamepad API</a> — referência completa da API</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API">MDN: Como usar a Gamepad API</a> — guia passo a passo</li>
<li><a href="https://gamepad-tester.com/">Testador de gamepad</a> — teste os mapeamentos do seu controle no navegador</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Boas práticas para incorporar jogos com iframe]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/iframe-embedding-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/iframe-embedding-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Incorpore jogos web corretamente em iframes: segurança, comunicação, tela cheia e requisitos das plataformas. Faça seu jogo ser incorporável em qualquer lugar.]]></description>
            <content:encoded><![CDATA[<h1>Boas práticas para incorporar jogos com iframe</h1>
<p>Muitas plataformas (<a href="http://Itch.io">Itch.io</a>, Newgrounds, Cinevva e seu próprio site) incorporam jogos em iframes. Este tutorial mostra como fazer seu jogo funcionar bem quando incorporado.</p>
<h2>1) Configuração básica para incorporação</h2>
<p>Seu jogo deve funcionar sem exigir controle total da página:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
  &lt;style&gt;
    * { margin: 0; padding: 0; }
    html, body { width: 100%; height: 100%; overflow: hidden; }
    canvas { display: block; width: 100%; height: 100%; }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;canvas id=&quot;game&quot;&gt;&lt;/canvas&gt;
  &lt;script src=&quot;game.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h2>2) Detectando o contexto de iframe</h2>
<pre><code class="language-js">const isEmbedded = window.self !== window.top

if (isEmbedded) {
  // Ajuste o comportamento para o contexto incorporado
  hideExternalLinks()
  adjustUIForSmallSize()
}

function hideExternalLinks() {
  document.querySelectorAll('a[target=&quot;_blank&quot;]').forEach(link =&gt; {
    link.style.display = 'none'
  })
}
</code></pre>
<h2>3) Permitindo as permissões necessárias no iframe</h2>
<p>A página principal controla o que seu iframe pode fazer. Permissões comuns:</p>
<pre><code class="language-html">&lt;iframe 
  src=&quot;https://game.example.com&quot;
  allow=&quot;fullscreen; autoplay; gamepad; pointer-lock&quot;
  sandbox=&quot;allow-scripts allow-same-origin allow-pointer-lock allow-popups&quot;
&gt;&lt;/iframe&gt;
</code></pre>
<p>Seu jogo deve lidar de forma adequada com permissões ausentes:</p>
<pre><code class="language-js">// Verifique se a tela cheia está disponível
const canFullscreen = document.fullscreenEnabled || document.webkitFullscreenEnabled

if (!canFullscreen) {
  document.getElementById('fullscreen-btn').style.display = 'none'
}
</code></pre>
<h2>4) Tela cheia a partir do iframe</h2>
<p>A tela cheia exige o atributo <code>allow=&quot;fullscreen&quot;</code>:</p>
<pre><code class="language-js">async function requestFullscreen() {
  const elem = document.documentElement
  
  try {
    if (elem.requestFullscreen) {
      await elem.requestFullscreen()
    } else if (elem.webkitRequestFullscreen) {
      await elem.webkitRequestFullscreen()
    }
  } catch (err) {
    // Tela cheia não permitida — exiba uma mensagem
    showMessage('Tela cheia indisponível quando o jogo está incorporado')
  }
}
</code></pre>
<h2>5) Comunicação com a página principal</h2>
<p>Use <code>postMessage</code> para uma comunicação segura entre origens:</p>
<p><strong>No seu jogo:</strong></p>
<pre><code class="language-js">// Envie uma mensagem à página principal
function notifyParent(type, data) {
  if (window.parent !== window) {
    window.parent.postMessage({ type, data, source: 'game' }, '*')
  }
}

// Receba mensagens da página principal
window.addEventListener('message', (event) =&gt; {
  // Valide a origem, se necessário
  // if (event.origin !== 'https://trusted-host.com') return
  
  const { type, data } = event.data
  
  switch (type) {
    case 'pause':
      pauseGame()
      break
    case 'resume':
      resumeGame()
      break
    case 'setVolume':
      setVolume(data.volume)
      break
  }
})

// Avise quando o jogo estiver pronto
window.addEventListener('load', () =&gt; {
  notifyParent('ready', { width: 800, height: 600 })
})

// Avise sobre eventos do jogo
function onGameOver(score) {
  notifyParent('gameover', { score })
}
</code></pre>
<p><strong>Na página principal:</strong></p>
<pre><code class="language-js">const iframe = document.getElementById('game-iframe')

iframe.addEventListener('load', () =&gt; {
  // Aguarde mensagens do jogo
  window.addEventListener('message', (event) =&gt; {
    if (event.source !== iframe.contentWindow) return
    if (event.data.source !== 'game') return
    
    const { type, data } = event.data
    
    if (type === 'ready') {
      console.log('Jogo pronto:', data)
    }
    if (type === 'gameover') {
      showScoreModal(data.score)
    }
  })
})

// Envie comandos ao jogo
function pauseGame() {
  iframe.contentWindow.postMessage({ type: 'pause' }, '*')
}
</code></pre>
<h2>6) Gerenciando o foco</h2>
<p>Iframes podem perder o foco, interrompendo a entrada pelo teclado:</p>
<pre><code class="language-js">// Coloque o canvas em foco automaticamente ao receber um clique
canvas.addEventListener('click', () =&gt; {
  canvas.focus()
})

// Permita que o canvas receba foco
canvas.tabIndex = 1

// Gerencie a perda de foco
window.addEventListener('blur', () =&gt; {
  // Redefina as teclas pressionadas
  input.left = input.right = input.up = input.down = false
  
  if (isEmbedded) {
    // Pause opcionalmente
    // pauseGame()
  }
})

// Solicite foco à página principal
function requestFocus() {
  notifyParent('requestFocus', {})
}
</code></pre>
<h2>7) Tamanho responsivo da incorporação</h2>
<p>Adapte-se a diferentes dimensões de incorporação:</p>
<pre><code class="language-js">function handleResize() {
  const width = window.innerWidth
  const height = window.innerHeight
  
  // Ajuste a interface com base no tamanho
  if (width &lt; 400 || height &lt; 300) {
    enableCompactUI()
  } else {
    enableFullUI()
  }
  
  // Redimensione o jogo adequadamente
  resizeCanvas(width, height)
}

window.addEventListener('resize', handleResize)
handleResize()
</code></pre>
<h2>8) Indicador de carregamento para incorporações</h2>
<p>Exiba algo imediatamente:</p>
<pre><code class="language-js">// Inclua diretamente no HTML para exibição instantânea
const loadingHTML = `
  &lt;div id=&quot;loading&quot; style=&quot;
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #1a1a2e;
    color: #fff;
    font-family: sans-serif;
  &quot;&gt;
    &lt;div&gt;
      &lt;div class=&quot;spinner&quot;&gt;&lt;/div&gt;
      &lt;p&gt;Carregando...&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
`

// Remova quando o jogo estiver pronto
function hideLoading() {
  const loading = document.getElementById('loading')
  if (loading) {
    loading.style.opacity = '0'
    loading.style.transition = 'opacity 0.3s'
    setTimeout(() =&gt; loading.remove(), 300)
  }
}
</code></pre>
<h2>9) SDKs específicos das plataformas</h2>
<p>Algumas plataformas têm APIs próprias:</p>
<p><strong><a href="http://Itch.io">Itch.io</a>:</strong></p>
<pre><code class="language-js">// Nenhum SDK é necessário, mas você pode usar postMessage para conquistas
window.parent.postMessage({ type: 'itch-achievement', data: { id: 'first-win' }}, '*')
</code></pre>
<p><strong>Newgrounds:</strong></p>
<pre><code class="language-js">// Inclua o SDK Newgrounds.io
const ngio = new Newgrounds.io.core('APP_ID', 'AES_KEY')
ngio.callComponent('Medal.unlock', { id: 12345 })
</code></pre>
<p><strong>Cinevva:</strong></p>
<pre><code class="language-js">// Sinalize que o jogo está pronto
window.parent.postMessage({ type: 'cinevva:ready' }, '*')

// Sinalize o estado jogável
window.parent.postMessage({ type: 'cinevva:playable' }, '*')
</code></pre>
<h2>10) Considerações de segurança</h2>
<pre><code class="language-js">// Valide as origens das mensagens em operações sensíveis
window.addEventListener('message', (event) =&gt; {
  const trustedOrigins = [
    'https://itch.io',
    'https://cinevva.com',
    'https://yoursite.com'
  ]
  
  if (!trustedOrigins.includes(event.origin)) {
    return // Ignore mensagens não confiáveis
  }
  
  // Processe a mensagem...
})

// Não exponha operações sensíveis via postMessage
// Permita apenas comandos incluídos na lista de permissões
const allowedCommands = ['pause', 'resume', 'setVolume', 'mute']

window.addEventListener('message', (event) =&gt; {
  const { type } = event.data
  if (!allowedCommands.includes(type)) return
  
  // Processe o comando...
})
</code></pre>
<h2>Checklist de testes</h2>
<ul>
<li><strong>Teste local</strong>: use um servidor HTTP simples, não <code>file://</code></li>
<li><strong>Entre origens</strong>: teste com o jogo realmente incorporado em um iframe</li>
<li><strong>Permissões</strong>: teste com um sandbox restrito</li>
<li><strong>Foco</strong>: teste o teclado depois de clicar fora do iframe</li>
<li><strong>Redimensionamento</strong>: teste com vários tamanhos de incorporação</li>
<li><strong>Dispositivos móveis</strong>: teste os controles por toque no contexto incorporado</li>
</ul>
<pre><code class="language-html">&lt;!-- Página de teste da incorporação --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body style=&quot;background: #333; padding: 20px;&quot;&gt;
  &lt;h1 style=&quot;color: #fff;&quot;&gt;Teste de incorporação&lt;/h1&gt;
  &lt;iframe 
    src=&quot;http://localhost:8000&quot; 
    width=&quot;800&quot; 
    height=&quot;600&quot;
    allow=&quot;fullscreen; autoplay; gamepad&quot;
  &gt;&lt;/iframe&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carrega rapidamente</a></li>
<li><a href="/pt-BR/tutorials/mobile-friendly-web-games">Jogos web otimizados para dispositivos móveis</a></li>
<li><a href="/pt-BR/creators">Para criadores</a></li>
<li><a href="/pt-BR/tutorials/coop-coep-sharedarraybuffer">COOP/COEP e SharedArrayBuffer</a> — cabeçalhos entre origens que afetam a incorporação em iframes</li>
<li><a href="/pt-BR/guides/itch-io-launch-guide">Como lançar seu jogo no itch.io</a> — o <a href="http://itch.io">itch.io</a> incorpora nativamente jogos de navegador em iframes</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe">MDN: elemento iframe</a> — referência completa dos atributos de iframe</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy">MDN: Política de Permissões</a> — controle dos recursos que os iframes podem usar</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage">MDN: API postMessage</a> — comunicação entre o iframe e a página principal</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Salve e carregue o estado do jogo com IndexedDB]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/indexeddb-game-saves</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/indexeddb-game-saves</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Saves persistentes de jogos com IndexedDB: armazenamento de progresso, configurações e grandes volumes de dados. Padrões assíncronos que não bloqueiam seu jogo.]]></description>
            <content:encoded><![CDATA[<h1>Salve e carregue o estado do jogo com IndexedDB</h1>
<p>IndexedDB é a melhor opção para armazenar dados persistentes de jogos no navegador. Ele lida com grandes volumes de dados, funciona offline e não bloqueia a thread principal.</p>
<h2>1) Por que usar IndexedDB em vez de localStorage?</h2>
<table>
<thead>
<tr>
<th>Recurso</th>
<th>localStorage</th>
<th>IndexedDB</th>
</tr>
</thead>
<tbody>
<tr>
<td>Limite de armazenamento</td>
<td>~5-10 MB</td>
<td>50+ MB (geralmente GB)</td>
</tr>
<tr>
<td>Tipos de dados</td>
<td>Apenas strings</td>
<td>Objetos, blobs, arrays</td>
</tr>
<tr>
<td>Assíncrono</td>
<td>Não (bloqueia)</td>
<td>Sim</td>
</tr>
<tr>
<td>Consultas indexadas</td>
<td>Não</td>
<td>Sim</td>
</tr>
</tbody>
</table>
<p>Para jogos, IndexedDB quase sempre é a escolha certa.</p>
<h2>2) Abrindo um banco de dados</h2>
<pre><code class="language-js">function openGameDB() {
  return new Promise((resolve, reject) =&gt; {
    const request = indexedDB.open('MyGame', 1)
    
    request.onerror = () =&gt; reject(request.error)
    request.onsuccess = () =&gt; resolve(request.result)
    
    request.onupgradeneeded = (event) =&gt; {
      const db = event.target.result
      
      // Cria os armazenamentos
      if (!db.objectStoreNames.contains('saves')) {
        db.createObjectStore('saves', { keyPath: 'slot' })
      }
      if (!db.objectStoreNames.contains('settings')) {
        db.createObjectStore('settings', { keyPath: 'key' })
      }
    }
  })
}
</code></pre>
<h2>3) Salvando o estado do jogo</h2>
<pre><code class="language-js">async function saveGame(slot, gameState) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) =&gt; {
    const tx = db.transaction('saves', 'readwrite')
    const store = tx.objectStore('saves')
    
    const saveData = {
      slot,
      state: gameState,
      timestamp: Date.now(),
    }
    
    const request = store.put(saveData)
    request.onsuccess = () =&gt; resolve()
    request.onerror = () =&gt; reject(request.error)
  })
}
</code></pre>
<h2>4) Carregando o estado do jogo</h2>
<pre><code class="language-js">async function loadGame(slot) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) =&gt; {
    const tx = db.transaction('saves', 'readonly')
    const store = tx.objectStore('saves')
    
    const request = store.get(slot)
    request.onsuccess = () =&gt; resolve(request.result?.state || null)
    request.onerror = () =&gt; reject(request.error)
  })
}
</code></pre>
<h2>5) Listando todos os saves</h2>
<pre><code class="language-js">async function listSaves() {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) =&gt; {
    const tx = db.transaction('saves', 'readonly')
    const store = tx.objectStore('saves')
    
    const request = store.getAll()
    request.onsuccess = () =&gt; resolve(request.result)
    request.onerror = () =&gt; reject(request.error)
  })
}
</code></pre>
<h2>6) Excluindo um save</h2>
<pre><code class="language-js">async function deleteSave(slot) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) =&gt; {
    const tx = db.transaction('saves', 'readwrite')
    const store = tx.objectStore('saves')
    
    const request = store.delete(slot)
    request.onsuccess = () =&gt; resolve()
    request.onerror = () =&gt; reject(request.error)
  })
}
</code></pre>
<h2>7) Uma classe GameStorage completa</h2>
<pre><code class="language-js">class GameStorage {
  constructor(dbName = 'GameData', version = 1) {
    this.dbName = dbName
    this.version = version
    this.db = null
  }
  
  async init() {
    this.db = await this.openDB()
  }
  
  openDB() {
    return new Promise((resolve, reject) =&gt; {
      const request = indexedDB.open(this.dbName, this.version)
      request.onerror = () =&gt; reject(request.error)
      request.onsuccess = () =&gt; resolve(request.result)
      request.onupgradeneeded = (e) =&gt; {
        const db = e.target.result
        if (!db.objectStoreNames.contains('saves')) {
          db.createObjectStore('saves', { keyPath: 'slot' })
        }
        if (!db.objectStoreNames.contains('settings')) {
          db.createObjectStore('settings', { keyPath: 'key' })
        }
        if (!db.objectStoreNames.contains('assets')) {
          db.createObjectStore('assets', { keyPath: 'url' })
        }
      }
    })
  }
  
  async save(slot, data) {
    const tx = this.db.transaction('saves', 'readwrite')
    tx.objectStore('saves').put({ slot, data, timestamp: Date.now() })
    return tx.complete
  }
  
  async load(slot) {
    const tx = this.db.transaction('saves', 'readonly')
    const result = await this.promisify(tx.objectStore('saves').get(slot))
    return result?.data || null
  }
  
  async setSetting(key, value) {
    const tx = this.db.transaction('settings', 'readwrite')
    tx.objectStore('settings').put({ key, value })
  }
  
  async getSetting(key, defaultValue = null) {
    const tx = this.db.transaction('settings', 'readonly')
    const result = await this.promisify(tx.objectStore('settings').get(key))
    return result?.value ?? defaultValue
  }
  
  promisify(request) {
    return new Promise((resolve, reject) =&gt; {
      request.onsuccess = () =&gt; resolve(request.result)
      request.onerror = () =&gt; reject(request.error)
    })
  }
}
</code></pre>
<h2>8) Armazenando dados binários (texturas, áudio)</h2>
<p>IndexedDB lida com Blobs e ArrayBuffers:</p>
<pre><code class="language-js">async function cacheAsset(url, blob) {
  const tx = db.transaction('assets', 'readwrite')
  tx.objectStore('assets').put({ url, blob, cached: Date.now() })
}

async function getCachedAsset(url) {
  const tx = db.transaction('assets', 'readonly')
  const result = await promisify(tx.objectStore('assets').get(url))
  return result?.blob || null
}
</code></pre>
<h2>9) Padrão de salvamento automático</h2>
<pre><code class="language-js">class AutoSave {
  constructor(storage, interval = 60000) {
    this.storage = storage
    this.interval = interval
    this.timer = null
    this.dirty = false
  }
  
  markDirty() {
    this.dirty = true
  }
  
  start(getState) {
    this.timer = setInterval(async () =&gt; {
      if (this.dirty) {
        await this.storage.save('autosave', getState())
        this.dirty = false
        console.log('Salvo automaticamente')
      }
    }, this.interval)
  }
  
  stop() {
    clearInterval(this.timer)
  }
}
</code></pre>
<h2>10) Tratamento de erros e alternativas</h2>
<pre><code class="language-js">async function safeLoad(slot, defaultState) {
  try {
    const saved = await loadGame(slot)
    if (saved) {
      // Valida/migra saves antigos, se necessário
      return migrateSave(saved)
    }
  } catch (err) {
    console.warn('Falha ao carregar o save:', err)
  }
  return defaultState
}

function migrateSave(save) {
  // Lida com formatos antigos de save
  if (!save.version) {
    save.version = 1
    save.settings = save.settings || {}
  }
  return save
}
</code></pre>
<h2>11) Faça os saves sobreviverem à remoção</h2>
<p>Não há garantia de que os dados do IndexedDB permanecerão armazenados. Por padrão, uma origem usa armazenamento de &quot;melhor esforço&quot;, e o navegador pode removê-lo quando o disco fica cheio ou, no Safari/WebKit, após um período sem interação do usuário com seu site. No caso de saves de jogos, esses são justamente os dados que você não quer perder.</p>
<p>Solicite armazenamento persistente para que o navegador não apague seus dados sem uma ação explícita do usuário:</p>
<pre><code class="language-js">async function makeStoragePersistent() {
  if (navigator.storage &amp;&amp; navigator.storage.persist) {
    const persisted = await navigator.storage.persist()
    console.log(persisted ? 'Os saves estão protegidos contra remoção' : 'Os saves podem ser removidos sob pressão de armazenamento')
    return persisted
  }
  return false
}
</code></pre>
<p>Os navegadores decidem se concedem essa permissão com base em sinais de engajamento — quanto o usuário interage com seu site e se ele está instalado como PWA —, portanto, não presuma que a solicitação sempre terá sucesso. Você também pode verificar quanto espaço está disponível antes de gravar saves grandes ou assets em cache:</p>
<pre><code class="language-js">async function checkStorage() {
  if (navigator.storage &amp;&amp; navigator.storage.estimate) {
    const { usage, quota } = await navigator.storage.estimate()
    console.log(`Usando ${usage} de ${quota} bytes`)
  }
}
</code></pre>
<p>Ambas as APIs exigem um contexto seguro (HTTPS ou localhost).</p>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/pwa-offline-games">PWA para jogos offline</a></li>
<li><a href="/pt-BR/tutorials/service-workers-caching">Service workers para cache de jogos</a></li>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rapidamente</a></li>
<li><a href="/pt-BR/tutorials/streaming-asset-loading">Carregamento de assets por streaming</a> — usando IndexedDB como cache de assets</li>
<li><a href="/pt-BR/tutorials/analytics-web-games">Análise de dados para jogos web</a> — acompanhando padrões de salvamento e carregamento para entender o comportamento dos jogadores</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">MDN: API IndexedDB</a> — referência completa da API</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB">MDN: Como usar IndexedDB</a> — guia passo a passo</li>
<li><a href="https://github.com/jakearchibald/idb">Biblioteca idb</a> — um pequeno wrapper de IndexedDB baseado em Promises</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Jogos para web compatíveis com dispositivos móveis]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/mobile-friendly-web-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/mobile-friendly-web-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Faça seus jogos para web funcionarem em dispositivos móveis: controles por toque, desempenho, viewport, bateria e design responsivo. Alcance a maior plataforma de jogos.]]></description>
            <content:encoded><![CDATA[<h1>Jogos para web compatíveis com dispositivos móveis</h1>
<p>Os dispositivos móveis são a maior plataforma de jogos. Alguns ajustes fazem seu jogo para web funcionar muito bem em celulares e tablets.</p>
<h2>1) Configuração da viewport</h2>
<p>Evite o zoom e garanta o dimensionamento correto:</p>
<pre><code class="language-html">&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&quot;&gt;
</code></pre>
<h2>2) Canvas em tela cheia</h2>
<pre><code class="language-css">* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000;
  touch-action: none; /* Impede gestos do navegador */
}

#game {
  display: block;
  width: 100%;
  height: 100%;
}
</code></pre>
<p>Se você dimensionar a interface com base na altura da viewport em vez de <code>height: 100%</code>, prefira as unidades dinâmicas de viewport <code>dvh</code>, <code>svh</code> e <code>lvh</code> em vez de <code>vh</code>. Elas levam em conta a barra de ferramentas retrátil/expansível do navegador móvel, que <code>100vh</code> ignora (no iOS, <code>100vh</code> corresponde à altura com a barra de ferramentas oculta, portanto a parte inferior do layout acaba cortada). Essas unidades alcançaram o status Baseline Widely Available e são compatíveis com Chrome/Edge 108+, Firefox 101+ e Safari/iOS Safari 15.4+, abrangendo cerca de 92% dos usuários.</p>
<h2>3) Tratamento de eventos de toque</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')

// Impede os comportamentos de toque padrão
canvas.addEventListener('touchstart', (e) =&gt; e.preventDefault(), { passive: false })
canvas.addEventListener('touchmove', (e) =&gt; e.preventDefault(), { passive: false })

// Rastreia os toques
const touches = new Map()

canvas.addEventListener('touchstart', (e) =&gt; {
  for (const touch of e.changedTouches) {
    touches.set(touch.identifier, {
      x: touch.clientX,
      y: touch.clientY,
      startX: touch.clientX,
      startY: touch.clientY,
    })
  }
})

canvas.addEventListener('touchmove', (e) =&gt; {
  for (const touch of e.changedTouches) {
    const t = touches.get(touch.identifier)
    if (t) {
      t.x = touch.clientX
      t.y = touch.clientY
    }
  }
})

canvas.addEventListener('touchend', (e) =&gt; {
  for (const touch of e.changedTouches) {
    touches.delete(touch.identifier)
  }
})
</code></pre>
<p>Se o jogo também precisar funcionar com mouse ou caneta, considere usar Pointer Events (<code>pointerdown</code>/<code>pointermove</code>/<code>pointerup</code>) em vez de eventos de toque. Agora, a MDN os recomenda como o modelo de entrada único e independente de dispositivo: um único fluxo de código processa toque, mouse e caneta, e o multitoque continua funcionando por meio de <code>pointerId</code> (o equivalente a <code>touch.identifier</code> usado acima). Os eventos de toque continuam sendo perfeitamente adequados para jogos exclusivos para telas sensíveis ao toque.</p>
<h2>4) Joystick virtual</h2>
<pre><code class="language-js">class VirtualJoystick {
  constructor(x, y, radius) {
    this.baseX = x
    this.baseY = y
    this.radius = radius
    this.knobRadius = radius * 0.4
    this.active = false
    this.touchId = null
    this.dx = 0
    this.dy = 0
  }
  
  handleTouchStart(touch) {
    const dist = Math.hypot(touch.clientX - this.baseX, touch.clientY - this.baseY)
    if (dist &lt; this.radius) {
      this.active = true
      this.touchId = touch.identifier
      this.updatePosition(touch.clientX, touch.clientY)
    }
  }
  
  handleTouchMove(touch) {
    if (touch.identifier === this.touchId) {
      this.updatePosition(touch.clientX, touch.clientY)
    }
  }
  
  handleTouchEnd(touch) {
    if (touch.identifier === this.touchId) {
      this.active = false
      this.touchId = null
      this.dx = 0
      this.dy = 0
    }
  }
  
  updatePosition(x, y) {
    let dx = x - this.baseX
    let dy = y - this.baseY
    const dist = Math.hypot(dx, dy)
    
    if (dist &gt; this.radius) {
      dx = (dx / dist) * this.radius
      dy = (dy / dist) * this.radius
    }
    
    this.dx = dx / this.radius
    this.dy = dy / this.radius
  }
  
  draw(ctx) {
    // Círculo da base
    ctx.beginPath()
    ctx.arc(this.baseX, this.baseY, this.radius, 0, Math.PI * 2)
    ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'
    ctx.fill()
    
    // Manopla
    const knobX = this.baseX + this.dx * this.radius
    const knobY = this.baseY + this.dy * this.radius
    ctx.beginPath()
    ctx.arc(knobX, knobY, this.knobRadius, 0, Math.PI * 2)
    ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'
    ctx.fill()
  }
  
  getInput() {
    return { x: this.dx, y: this.dy }
  }
}
</code></pre>
<h2>5) Botões de toque</h2>
<pre><code class="language-js">class TouchButton {
  constructor(x, y, radius, label) {
    this.x = x
    this.y = y
    this.radius = radius
    this.label = label
    this.pressed = false
    this.touchId = null
  }
  
  contains(px, py) {
    return Math.hypot(px - this.x, py - this.y) &lt; this.radius
  }
  
  handleTouchStart(touch) {
    if (this.contains(touch.clientX, touch.clientY)) {
      this.pressed = true
      this.touchId = touch.identifier
    }
  }
  
  handleTouchEnd(touch) {
    if (touch.identifier === this.touchId) {
      this.pressed = false
      this.touchId = null
    }
  }
  
  draw(ctx) {
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
    ctx.fillStyle = this.pressed ? 'rgba(255, 255, 255, 0.5)' : 'rgba(255, 255, 255, 0.2)'
    ctx.fill()
    
    ctx.font = 'bold 24px sans-serif'
    ctx.fillStyle = '#fff'
    ctx.textAlign = 'center'
    ctx.textBaseline = 'middle'
    ctx.fillText(this.label, this.x, this.y)
  }
}
</code></pre>
<h2>6) Tratamento da orientação</h2>
<pre><code class="language-js">function checkOrientation() {
  const isLandscape = window.innerWidth &gt; window.innerHeight
  
  if (!isLandscape) {
    showRotatePrompt()
  } else {
    hideRotatePrompt()
    resizeCanvas()
  }
}

window.addEventListener('resize', checkOrientation)
window.addEventListener('orientationchange', checkOrientation)

function showRotatePrompt() {
  document.getElementById('rotate-prompt').style.display = 'flex'
}

function hideRotatePrompt() {
  document.getElementById('rotate-prompt').style.display = 'none'
}
</code></pre>
<h2>7) Otimização de desempenho</h2>
<pre><code class="language-js">// Reduz a resolução em dispositivos de baixo desempenho
function getDeviceScale() {
  const dpr = window.devicePixelRatio || 1
  const isLowEnd = navigator.hardwareConcurrency &lt;= 2
  return isLowEnd ? Math.min(dpr, 1.5) : Math.min(dpr, 2)
}

// Limita a frequência de operações pesadas
let lastHeavyUpdate = 0
function maybeDoHeavyWork(timestamp) {
  if (timestamp - lastHeavyUpdate &gt; 100) { // 10 fps para tarefas pesadas
    doExpensiveCalculations()
    lastHeavyUpdate = timestamp
  }
}

// Usa recursos de qualidade inferior
function getAssetQuality() {
  const memory = navigator.deviceMemory || 4
  if (memory &lt;= 2) return 'low'
  if (memory &lt;= 4) return 'medium'
  return 'high'
}
</code></pre>
<h2>8) Cuidados com a bateria</h2>
<pre><code class="language-js">// Verifica a bateria e reduz os efeitos
async function checkBattery() {
  if ('getBattery' in navigator) {
    const battery = await navigator.getBattery()
    
    if (battery.level &lt; 0.2 &amp;&amp; !battery.charging) {
      enableBatterySaver()
    }
    
    battery.addEventListener('levelchange', () =&gt; {
      if (battery.level &lt; 0.2 &amp;&amp; !battery.charging) {
        enableBatterySaver()
      }
    })
  }
}

function enableBatterySaver() {
  game.particleCount = Math.floor(game.particleCount * 0.5)
  game.targetFps = 30
  console.log('Modo de economia de bateria ativado')
}
</code></pre>
<h2>9) API de tela cheia</h2>
<pre><code class="language-js">async function requestFullscreen() {
  const elem = document.documentElement
  
  try {
    if (elem.requestFullscreen) {
      await elem.requestFullscreen()
    } else if (elem.webkitRequestFullscreen) {
      await elem.webkitRequestFullscreen()
    }
    
    // Bloqueia a orientação
    if (screen.orientation?.lock) {
      await screen.orientation.lock('landscape')
    }
  } catch (err) {
    console.warn('Falha ao ativar a tela cheia:', err)
  }
}

// Chama durante uma interação do usuário
document.getElementById('play-btn').addEventListener('click', () =&gt; {
  requestFullscreen()
  startGame()
})
</code></pre>
<p>Uma ressalva: a API de tela cheia não funciona para elementos comuns no iPhone. O Safari do iOS só oferece tela cheia para elementos de vídeo nativos (por meio do método não padronizado <code>webkitEnterFullscreen</code>), portanto <code>documentElement.requestFullscreen()</code> será rejeitado no iPhone. O <code>try/catch</code> acima trata isso sem interromper o jogo. Por isso, projete o jogo para ser jogável sem tela cheia de verdade no iPhone e considere a tela cheia um aprimoramento para iPad, Android e computadores, onde há suporte. A chamada <code>screen.orientation.lock()</code> tem a mesma limitação no iPhone, portanto não dependa dela.</p>
<h2>10) Checklist de testes</h2>
<p>Antes de publicar:</p>
<ul>
<li><strong>Resposta ao toque</strong> — Teste em dispositivos reais, não apenas em emuladores</li>
<li><strong>Vários tamanhos de tela</strong> — Celular, tablet, iPad Pro</li>
<li><strong>Ambas as orientações</strong> — Ou bloqueie em uma delas</li>
<li><strong>Dispositivos de baixo desempenho</strong> — Teste em celulares com 2 a 3 anos de uso</li>
<li><strong>Redes lentas</strong> — Teste com simulação de 3G</li>
<li><strong>Safari no iOS</strong> — Tem particularidades com áudio e tela cheia</li>
<li><strong>Chrome no Android</strong> — O navegador mais comum</li>
<li><strong>Interrupções</strong> — Ligações telefônicas, notificações</li>
<li><strong>Pressão sobre a memória</strong> — O jogo se recupera depois que a aba fica em segundo plano?</li>
</ul>
<pre><code class="language-js">// Visualização de depuração dos toques
function drawTouchDebug(ctx) {
  for (const [id, touch] of touches) {
    ctx.beginPath()
    ctx.arc(touch.x, touch.y, 30, 0, Math.PI * 2)
    ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
    ctx.fill()
    ctx.fillStyle = '#fff'
    ctx.fillText(id.toString(), touch.x, touch.y)
  }
}
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/game-input-handling">Tratamento de entradas em jogos</a></li>
<li><a href="/pt-BR/tutorials/responsive-game-canvas">Canvas responsivo para jogos</a></li>
<li><a href="/pt-BR/tutorials/pwa-offline-games">PWA para jogos offline</a></li>
<li><a href="/pt-BR/tutorials/web-audio-api-games">Web Audio API para jogos</a> — como lidar com as restrições de áudio do iOS</li>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo para web que carregue rapidamente</a> — otimização do tempo de carregamento em conexões móveis</li>
<li><a href="/pt-BR/tutorials/iframe-embedding-games">Incorporação de jogos com iframe</a> — aspectos específicos da incorporação em dispositivos móveis</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Touch_events">MDN: eventos de toque</a> — tratamento de multitoque</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API">MDN: API Screen Orientation</a> — bloqueio da orientação</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API">MDN: API Fullscreen</a> — como entrar em tela cheia em dispositivos móveis</li>
<li><a href="https://web.dev/learn/design/">web.dev: design responsivo</a> — padrões responsivos aplicáveis à interface de jogos</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Renderização de pixel art em jogos web]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/pixel-art-rendering</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/pixel-art-rendering</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Renderize pixel art corretamente: dimensionamento nítido, sem desfoque, paletas de cores e animação. Faça seu jogo retrô parecer intencional.]]></description>
            <content:encoded><![CDATA[<h1>Renderização de pixel art em jogos web</h1>
<p>Pixel art fica incrível quando renderizada corretamente — e péssima quando está borrada. Este tutorial aborda as técnicas para criar gráficos em pixels nítidos e autênticos.</p>
<h2>1) O problema do desfoque</h2>
<p>Por padrão, os navegadores suavizam as imagens ao redimensioná-las. Para pixel art, isso é inadequado:</p>
<pre><code class="language-css">/* CSS solution */
canvas {
  image-rendering: pixelated;
  image-rendering: crisp-edges; /* Firefox fallback */
}
</code></pre>
<h2>2) Configurações do contexto do Canvas</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')

// Disable smoothing for crisp pixels
ctx.imageSmoothingEnabled = false
</code></pre>
<p>Essa configuração deve ser aplicada novamente após cada redimensionamento do Canvas ou alteração de contexto.</p>
<h2>3) Escalonamento por números inteiros</h2>
<p>Pixel art fica melhor em escalas inteiras (1x, 2x, 3x, 4x):</p>
<pre><code class="language-js">const GAME_WIDTH = 160
const GAME_HEIGHT = 144 // Game Boy resolution

function resize() {
  const maxScale = Math.floor(Math.min(
    window.innerWidth / GAME_WIDTH,
    window.innerHeight / GAME_HEIGHT
  ))
  const scale = Math.max(1, maxScale)
  
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
  canvas.style.width = GAME_WIDTH * scale + 'px'
  canvas.style.height = GAME_HEIGHT * scale + 'px'
  
  ctx.imageSmoothingEnabled = false
}
</code></pre>
<h2>4) Desenhando sprites em posições inteiras</h2>
<p>Posições de subpixel causam desfoque:</p>
<pre><code class="language-js">// Bad: causes blur
ctx.drawImage(sprite, 10.5, 20.3)

// Good: crisp pixels
ctx.drawImage(sprite, Math.floor(10.5), Math.floor(20.3))

// Or round to nearest
ctx.drawImage(sprite, Math.round(player.x), Math.round(player.y))
</code></pre>
<h2>5) Animação com sprite sheet</h2>
<pre><code class="language-js">class PixelSprite {
  constructor(image, frameWidth, frameHeight) {
    this.image = image
    this.frameWidth = frameWidth
    this.frameHeight = frameHeight
    this.animations = {}
    this.currentAnim = null
    this.frame = 0
    this.timer = 0
  }
  
  addAnimation(name, frames, fps = 8) {
    this.animations[name] = { frames, duration: 1 / fps }
  }
  
  play(name) {
    if (this.currentAnim !== name) {
      this.currentAnim = name
      this.frame = 0
      this.timer = 0
    }
  }
  
  update(dt) {
    if (!this.currentAnim) return
    
    const anim = this.animations[this.currentAnim]
    this.timer += dt
    
    if (this.timer &gt;= anim.duration) {
      this.timer = 0
      this.frame = (this.frame + 1) % anim.frames.length
    }
  }
  
  draw(ctx, x, y, flipX = false) {
    if (!this.currentAnim) return
    
    const anim = this.animations[this.currentAnim]
    const frameIndex = anim.frames[this.frame]
    const sx = (frameIndex % 16) * this.frameWidth
    const sy = Math.floor(frameIndex / 16) * this.frameHeight
    
    ctx.save()
    if (flipX) {
      ctx.scale(-1, 1)
      x = -x - this.frameWidth
    }
    
    ctx.drawImage(
      this.image,
      sx, sy, this.frameWidth, this.frameHeight,
      Math.round(x), Math.round(y), this.frameWidth, this.frameHeight
    )
    ctx.restore()
  }
}

// Usage
const player = new PixelSprite(spriteSheet, 16, 16)
player.addAnimation('idle', [0, 1], 2)
player.addAnimation('walk', [2, 3, 4, 5], 8)
player.addAnimation('jump', [6], 1)
</code></pre>
<h2>6) Troca de paleta</h2>
<p>Uma técnica clássica para recolorir sprites:</p>
<pre><code class="language-js">function createPaletteSwap(image, oldColors, newColors) {
  const canvas = document.createElement('canvas')
  canvas.width = image.width
  canvas.height = image.height
  const ctx = canvas.getContext('2d')
  
  ctx.drawImage(image, 0, 0)
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  const data = imageData.data
  
  for (let i = 0; i &lt; data.length; i += 4) {
    const r = data[i], g = data[i + 1], b = data[i + 2]
    
    for (let j = 0; j &lt; oldColors.length; j++) {
      const old = oldColors[j]
      if (r === old.r &amp;&amp; g === old.g &amp;&amp; b === old.b) {
        const newC = newColors[j]
        data[i] = newC.r
        data[i + 1] = newC.g
        data[i + 2] = newC.b
        break
      }
    }
  }
  
  ctx.putImageData(imageData, 0, 0)
  return canvas
}

// Create enemy variant
const blueEnemy = createPaletteSwap(
  enemySprite,
  [{ r: 255, g: 0, b: 0 }],    // Red
  [{ r: 0, g: 100, b: 255 }]   // Blue
)
</code></pre>
<h2>7) Renderização de mapas de tiles</h2>
<pre><code class="language-js">class Tilemap {
  constructor(tileSize, tilesetImage) {
    this.tileSize = tileSize
    this.tileset = tilesetImage
    this.tilesPerRow = Math.floor(tilesetImage.width / tileSize)
    this.data = []
  }
  
  setData(width, height, tiles) {
    this.width = width
    this.height = height
    this.data = tiles
  }
  
  draw(ctx, cameraX = 0, cameraY = 0) {
    const startCol = Math.floor(cameraX / this.tileSize)
    const startRow = Math.floor(cameraY / this.tileSize)
    const endCol = startCol + Math.ceil(ctx.canvas.width / this.tileSize) + 1
    const endRow = startRow + Math.ceil(ctx.canvas.height / this.tileSize) + 1
    
    for (let row = startRow; row &lt; endRow; row++) {
      for (let col = startCol; col &lt; endCol; col++) {
        if (row &lt; 0 || row &gt;= this.height || col &lt; 0 || col &gt;= this.width) continue
        
        const tileIndex = this.data[row * this.width + col]
        if (tileIndex &lt; 0) continue
        
        const sx = (tileIndex % this.tilesPerRow) * this.tileSize
        const sy = Math.floor(tileIndex / this.tilesPerRow) * this.tileSize
        const dx = Math.round(col * this.tileSize - cameraX)
        const dy = Math.round(row * this.tileSize - cameraY)
        
        ctx.drawImage(
          this.tileset,
          sx, sy, this.tileSize, this.tileSize,
          dx, dy, this.tileSize, this.tileSize
        )
      }
    }
  }
}
</code></pre>
<h2>8) Efeitos de contorno</h2>
<p>Adicione contornos aos sprites para melhorar a visibilidade:</p>
<pre><code class="language-js">function drawWithOutline(ctx, image, x, y, outlineColor = '#000') {
  const offsets = [[-1, 0], [1, 0], [0, -1], [0, 1]]
  
  // Draw outline
  ctx.globalCompositeOperation = 'source-over'
  for (const [ox, oy] of offsets) {
    ctx.drawImage(image, Math.round(x + ox), Math.round(y + oy))
  }
  
  // Apply color to outline
  ctx.globalCompositeOperation = 'source-in'
  ctx.fillStyle = outlineColor
  ctx.fillRect(x - 1, y - 1, image.width + 2, image.height + 2)
  
  // Draw sprite on top
  ctx.globalCompositeOperation = 'source-over'
  ctx.drawImage(image, Math.round(x), Math.round(y))
}
</code></pre>
<h2>9) Tremor de tela</h2>
<p>Um efeito clássico para melhorar a sensação do jogo:</p>
<pre><code class="language-js">const camera = {
  x: 0,
  y: 0,
  shakeIntensity: 0,
  shakeDuration: 0,
  
  shake(intensity, duration) {
    this.shakeIntensity = intensity
    this.shakeDuration = duration
  },
  
  update(dt) {
    if (this.shakeDuration &gt; 0) {
      this.shakeDuration -= dt
    }
  },
  
  getOffset() {
    if (this.shakeDuration &lt;= 0) return { x: 0, y: 0 }
    
    return {
      x: Math.round((Math.random() - 0.5) * 2 * this.shakeIntensity),
      y: Math.round((Math.random() - 0.5) * 2 * this.shakeIntensity),
    }
  }
}

// In render:
function render() {
  const shake = camera.getOffset()
  ctx.save()
  ctx.translate(shake.x, shake.y)
  // Draw everything...
  ctx.restore()
}

// On hit:
camera.shake(4, 0.2) // 4 pixel intensity, 0.2 seconds
</code></pre>
<h2>10) Limitações da paleta de cores</h2>
<p>Pixel art autêntica usa paletas limitadas:</p>
<pre><code class="language-js">const PALETTES = {
  gameboy: ['#0f380f', '#306230', '#8bac0f', '#9bbc0f'],
  nes: ['#000000', '#fcfcfc', '#f8f8f8', '#bcbcbc', /* ... */],
  pico8: [
    '#000000', '#1d2b53', '#7e2553', '#008751',
    '#ab5236', '#5f574f', '#c2c3c7', '#fff1e8',
    '#ff004d', '#ffa300', '#ffec27', '#00e436',
    '#29adff', '#83769c', '#ff77a8', '#ffccaa'
  ],
}

function quantizeColor(r, g, b, palette) {
  let minDist = Infinity
  let closest = palette[0]
  
  for (const color of palette) {
    const pr = parseInt(color.slice(1, 3), 16)
    const pg = parseInt(color.slice(3, 5), 16)
    const pb = parseInt(color.slice(5, 7), 16)
    
    const dist = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
    if (dist &lt; minDist) {
      minDist = dist
      closest = color
    }
  }
  
  return closest
}
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/canvas-2d-game-loop">Loop de jogo com Canvas 2D</a></li>
<li><a href="/pt-BR/tutorials/responsive-game-canvas">Canvas responsivo para jogos</a></li>
<li><a href="/pt-BR/tutorials/streaming-asset-loading">Carregamento de assets por streaming</a></li>
<li><a href="/pt-BR/guides/game-assets-guide">Onde encontrar assets gratuitos para jogos</a> — sprite sheets, pacotes de pixel art e fontes de assets 2D</li>
<li><a href="/pt-BR/tutorials/text-rendering-games">Renderização de texto em jogos</a> — fontes bitmap que combinam com o estilo da sua pixel art</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Pointer Lock para jogos FPS]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/pointer-lock-fps</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/pointer-lock-fps</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Implemente a visão com o mouse usando a API Pointer Lock: capture o cursor, processe o movimento e crie controles FPS para jogos web.]]></description>
            <content:encoded><![CDATA[<h1>Pointer Lock para jogos FPS</h1>
<p>A API Pointer Lock captura o cursor do mouse, permitindo controles suaves de câmera no estilo FPS. Essencial para jogos de tiro em primeira e terceira pessoa.</p>
<h2>1) Solicitando o bloqueio do ponteiro</h2>
<p>O bloqueio do ponteiro exige uma interação do usuário:</p>
<pre><code class="language-js">const canvas = document.getElementById('game')

canvas.addEventListener('click', () =&gt; {
  canvas.requestPointerLock()
})
</code></pre>
<h2>Entrada bruta do mouse (desativar aceleração)</h2>
<p>Por padrão, o navegador aplica a aceleração do mouse do sistema operacional a <code>movementX</code>/<code>movementY</code>. Assim, um movimento rápido e um arrasto lento pela mesma distância produzem rotações diferentes da câmera. Para mirar em jogos FPS, geralmente você quer o contrário: a mesma distância física deve sempre girar a câmera na mesma proporção. Solicite movimentos brutos e sem aceleração passando <code>unadjustedMovement: true</code> para <code>requestPointerLock()</code>:</p>
<pre><code class="language-js">canvas.addEventListener('click', async () =&gt; {
  try {
    await canvas.requestPointerLock({ unadjustedMovement: true })
  } catch (err) {
    // unadjustedMovement não é compatível aqui; use deltas ajustados pelo sistema operacional
    await canvas.requestPointerLock()
  }
})
</code></pre>
<p>A versão moderna de <code>requestPointerLock()</code> retorna uma Promise que é resolvida em caso de sucesso e rejeitada em caso de falha, por isso a chamada acima está envolvida em <code>try/catch</code>. Chrome e Edge oferecem suporte a <code>unadjustedMovement</code> desde a versão 88, e o Safari desde a versão 18.4 (março de 2025), que também corrigiu <code>requestPointerLock</code> para retornar uma Promise. Navegadores mais antigos que não retornam uma Promise ainda funcionam aqui, pois <code>await</code> aceita um valor que não seja uma Promise e a opção desconhecida é simplesmente ignorada. Portanto, sempre mantenha a chamada simples a <code>requestPointerLock()</code> como alternativa.</p>
<h2>2) Detectando o estado do bloqueio</h2>
<pre><code class="language-js">document.addEventListener('pointerlockchange', () =&gt; {
  if (document.pointerLockElement === canvas) {
    console.log('Ponteiro bloqueado')
    game.mouseLocked = true
  } else {
    console.log('Ponteiro desbloqueado')
    game.mouseLocked = false
  }
})

document.addEventListener('pointerlockerror', () =&gt; {
  console.error('Falha ao bloquear o ponteiro')
})
</code></pre>
<h2>3) Lendo o movimento do mouse</h2>
<p>Quando o ponteiro estiver bloqueado, use <code>movementX</code> e <code>movementY</code>:</p>
<pre><code class="language-js">document.addEventListener('mousemove', (e) =&gt; {
  if (document.pointerLockElement !== canvas) return
  
  const sensitivity = 0.002
  camera.yaw -= e.movementX * sensitivity
  camera.pitch -= e.movementY * sensitivity
  
  // Limite a inclinação para evitar que a câmera vire de cabeça para baixo
  camera.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.pitch))
})
</code></pre>
<h2>4) Classe de câmera FPS</h2>
<pre><code class="language-js">class FPSCamera {
  constructor() {
    this.position = { x: 0, y: 1.7, z: 0 } // Altura dos olhos
    this.yaw = 0      // Rotação para esquerda/direita
    this.pitch = 0    // Rotação para cima/baixo
    this.sensitivity = 0.002
  }
  
  handleMouseMove(e) {
    this.yaw -= e.movementX * this.sensitivity
    this.pitch -= e.movementY * this.sensitivity
    this.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.pitch))
  }
  
  getForward() {
    return {
      x: Math.sin(this.yaw) * Math.cos(this.pitch),
      y: Math.sin(this.pitch),
      z: Math.cos(this.yaw) * Math.cos(this.pitch),
    }
  }
  
  getRight() {
    return {
      x: Math.cos(this.yaw),
      y: 0,
      z: -Math.sin(this.yaw),
    }
  }
  
  move(forward, right, dt, speed = 5) {
    const fwd = this.getForward()
    const rgt = this.getRight()
    
    // Mova apenas no plano XZ
    this.position.x += (fwd.x * forward + rgt.x * right) * speed * dt
    this.position.z += (fwd.z * forward + rgt.z * right) * speed * dt
  }
  
  getViewMatrix() {
    const forward = this.getForward()
    const target = {
      x: this.position.x + forward.x,
      y: this.position.y + forward.y,
      z: this.position.z + forward.z,
    }
    return lookAt(this.position, target, { x: 0, y: 1, z: 0 })
  }
}
</code></pre>
<h2>5) Movimento com WASD</h2>
<pre><code class="language-js">const input = {
  forward: false,
  backward: false,
  left: false,
  right: false,
  jump: false,
}

window.addEventListener('keydown', (e) =&gt; {
  switch (e.code) {
    case 'KeyW': input.forward = true; break
    case 'KeyS': input.backward = true; break
    case 'KeyA': input.left = true; break
    case 'KeyD': input.right = true; break
    case 'Space': input.jump = true; break
  }
})

window.addEventListener('keyup', (e) =&gt; {
  switch (e.code) {
    case 'KeyW': input.forward = false; break
    case 'KeyS': input.backward = false; break
    case 'KeyA': input.left = false; break
    case 'KeyD': input.right = false; break
    case 'Space': input.jump = false; break
  }
})

function update(dt) {
  const moveForward = (input.forward ? 1 : 0) - (input.backward ? 1 : 0)
  const moveRight = (input.right ? 1 : 0) - (input.left ? 1 : 0)
  
  camera.move(moveForward, moveRight, dt)
}
</code></pre>
<h2>6) Controlador FPS completo</h2>
<pre><code class="language-js">class FPSController {
  constructor(canvas) {
    this.canvas = canvas
    this.camera = new FPSCamera()
    this.locked = false
    this.speed = 5
    this.sprintMultiplier = 1.5
    
    this.input = {
      forward: false, backward: false,
      left: false, right: false,
      jump: false, sprint: false,
    }
    
    this.velocity = { x: 0, y: 0, z: 0 }
    this.onGround = true
    this.gravity = -20
    this.jumpSpeed = 8
    
    this.bindEvents()
  }
  
  bindEvents() {
    this.canvas.addEventListener('click', () =&gt; {
      this.canvas.requestPointerLock()
    })
    
    document.addEventListener('pointerlockchange', () =&gt; {
      this.locked = document.pointerLockElement === this.canvas
    })
    
    document.addEventListener('mousemove', (e) =&gt; {
      if (this.locked) {
        this.camera.handleMouseMove(e)
      }
    })
    
    window.addEventListener('keydown', (e) =&gt; this.handleKey(e.code, true))
    window.addEventListener('keyup', (e) =&gt; this.handleKey(e.code, false))
  }
  
  handleKey(code, pressed) {
    switch (code) {
      case 'KeyW': this.input.forward = pressed; break
      case 'KeyS': this.input.backward = pressed; break
      case 'KeyA': this.input.left = pressed; break
      case 'KeyD': this.input.right = pressed; break
      case 'Space': this.input.jump = pressed; break
      case 'ShiftLeft': this.input.sprint = pressed; break
    }
  }
  
  update(dt) {
    if (!this.locked) return
    
    const speed = this.speed * (this.input.sprint ? this.sprintMultiplier : 1)
    
    // Movimento horizontal
    const moveForward = (this.input.forward ? 1 : 0) - (this.input.backward ? 1 : 0)
    const moveRight = (this.input.right ? 1 : 0) - (this.input.left ? 1 : 0)
    
    const forward = this.camera.getForward()
    const right = this.camera.getRight()
    
    this.velocity.x = (forward.x * moveForward + right.x * moveRight) * speed
    this.velocity.z = (forward.z * moveForward + right.z * moveRight) * speed
    
    // Salto
    if (this.input.jump &amp;&amp; this.onGround) {
      this.velocity.y = this.jumpSpeed
      this.onGround = false
    }
    
    // Gravidade
    if (!this.onGround) {
      this.velocity.y += this.gravity * dt
    }
    
    // Aplique a velocidade
    this.camera.position.x += this.velocity.x * dt
    this.camera.position.y += this.velocity.y * dt
    this.camera.position.z += this.velocity.z * dt
    
    // Colisão simples com o chão
    if (this.camera.position.y &lt; 1.7) {
      this.camera.position.y = 1.7
      this.velocity.y = 0
      this.onGround = true
    }
  }
}
</code></pre>
<h2>7) Mira</h2>
<pre><code class="language-js">function drawCrosshair(ctx) {
  const cx = ctx.canvas.width / 2
  const cy = ctx.canvas.height / 2
  const size = 10
  const gap = 4
  
  ctx.strokeStyle = '#fff'
  ctx.lineWidth = 2
  
  // Superior
  ctx.beginPath()
  ctx.moveTo(cx, cy - gap)
  ctx.lineTo(cx, cy - gap - size)
  ctx.stroke()
  
  // Inferior
  ctx.beginPath()
  ctx.moveTo(cx, cy + gap)
  ctx.lineTo(cx, cy + gap + size)
  ctx.stroke()
  
  // Esquerda
  ctx.beginPath()
  ctx.moveTo(cx - gap, cy)
  ctx.lineTo(cx - gap - size, cy)
  ctx.stroke()
  
  // Direita
  ctx.beginPath()
  ctx.moveTo(cx + gap, cy)
  ctx.lineTo(cx + gap + size, cy)
  ctx.stroke()
}
</code></pre>
<h2>8) Atirando com os botões do mouse</h2>
<pre><code class="language-js">document.addEventListener('mousedown', (e) =&gt; {
  if (!document.pointerLockElement) return
  
  if (e.button === 0) {
    // Botão esquerdo — disparo primário
    weapon.fire()
  } else if (e.button === 2) {
    // Botão direito — mirar pela mira da arma
    weapon.aimDownSights(true)
  }
})

document.addEventListener('mouseup', (e) =&gt; {
  if (e.button === 2) {
    weapon.aimDownSights(false)
  }
})

// Impeça a exibição do menu de contexto
canvas.addEventListener('contextmenu', (e) =&gt; e.preventDefault())
</code></pre>
<h2>9) Configurações de sensibilidade</h2>
<pre><code class="language-js">class Settings {
  constructor() {
    this.mouseSensitivity = parseFloat(localStorage.getItem('sensitivity') || '1.0')
    this.invertY = localStorage.getItem('invertY') === 'true'
  }
  
  save() {
    localStorage.setItem('sensitivity', this.mouseSensitivity.toString())
    localStorage.setItem('invertY', this.invertY.toString())
  }
}

// Aplique no manipulador do mouse
document.addEventListener('mousemove', (e) =&gt; {
  if (!locked) return
  
  const sens = settings.mouseSensitivity * 0.002
  camera.yaw -= e.movementX * sens
  camera.pitch -= e.movementY * sens * (settings.invertY ? -1 : 1)
})
</code></pre>
<h2>10) Aviso de saída</h2>
<p>Mostre instruções para sair do bloqueio do ponteiro:</p>
<pre><code class="language-js">function showLockUI() {
  const ui = document.getElementById('lock-ui')
  
  if (document.pointerLockElement) {
    ui.innerHTML = '&lt;p&gt;Pressione ESC para liberar o mouse&lt;/p&gt;'
    ui.style.opacity = '0.5'
    setTimeout(() =&gt; ui.style.opacity = '0', 2000)
  } else {
    ui.innerHTML = '&lt;p&gt;Clique para jogar&lt;/p&gt;'
    ui.style.opacity = '1'
  }
}

document.addEventListener('pointerlockchange', showLockUI)
</code></pre>
<p>Um detalhe importante sobre o aviso de saída: quando o jogador pressiona ESC para sair do bloqueio do ponteiro, o navegador exige uma nova interação do usuário (um clique) antes de permitir outro bloqueio. É por isso que a sobreposição “Clique para jogar” existe. Porém, se o próprio código chamar <code>document.exitPointerLock()</code> (por exemplo, para abrir um menu dentro do jogo), nenhuma nova interação será necessária para bloquear novamente depois. Assim, você poderá reativar o bloqueio por código quando o menu for fechado. Pressionar ESC repetidamente também pode fazer o navegador recusar novas tentativas de bloqueio até que o usuário realize uma ação mais deliberada. Portanto, não tente bloquear o ponteiro automaticamente em <code>pointerlockchange</code>.</p>
<h2>Considerações sobre iframes</h2>
<p>O bloqueio do ponteiro em iframes exige o atributo <code>allow=&quot;pointer-lock&quot;</code>:</p>
<pre><code class="language-html">&lt;iframe 
  src=&quot;game.html&quot; 
  allow=&quot;pointer-lock; fullscreen&quot;
&gt;&lt;/iframe&gt;
</code></pre>
<p>Verifique se o bloqueio do ponteiro está disponível:</p>
<pre><code class="language-js">if (!document.pointerLockElement &amp;&amp; !('requestPointerLock' in canvas)) {
  showMessage('A captura do mouse não está disponível')
}
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/game-input-handling">Tratamento de entrada em jogos</a></li>
<li><a href="/pt-BR/tutorials/gamepad-api">API Gamepad</a></li>
<li><a href="/pt-BR/tutorials/webgl-fundamentals-for-games">Fundamentos de WebGL</a></li>
<li><a href="/pt-BR/tutorials/webxr-basics">Conceitos básicos de WebXR</a> — controles de RV para experiências em primeira pessoa</li>
<li><a href="/pt-BR/tutorials/webgpu-getting-started">Primeiros passos com WebGPU</a> — renderização moderna por GPU para jogos FPS</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API">MDN: API Pointer Lock</a> — referência completa da API</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX">MDN: MouseEvent.movementX</a> — leitura dos deltas do mouse</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PWA para jogos web offline]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/pwa-offline-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/pwa-offline-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Faça seu jogo funcionar offline com PWA: manifesto, service worker, estratégias de cache e prompts de instalação. Jogue em qualquer lugar, a qualquer hora.]]></description>
            <content:encoded><![CDATA[<h1>PWA para jogos web offline</h1>
<p>Os Progressive Web Apps permitem que os jogadores instalem seu jogo e joguem offline. Este tutorial mostra como adicionar suporte a PWA a um jogo web.</p>
<h2>1) Manifesto do aplicativo web</h2>
<p>Crie <code>manifest.json</code>:</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;Meu Jogo Incrível&quot;,
  &quot;short_name&quot;: &quot;JogoIncrível&quot;,
  &quot;description&quot;: &quot;Um jogo incrível que você pode jogar offline&quot;,
  &quot;start_url&quot;: &quot;/&quot;,
  &quot;display&quot;: &quot;fullscreen&quot;,
  &quot;orientation&quot;: &quot;landscape&quot;,
  &quot;background_color&quot;: &quot;#1a1a2e&quot;,
  &quot;theme_color&quot;: &quot;#4ade80&quot;,
  &quot;icons&quot;: [
    {
      &quot;src&quot;: &quot;/icons/icon-192.png&quot;,
      &quot;sizes&quot;: &quot;192x192&quot;,
      &quot;type&quot;: &quot;image/png&quot;
    },
    {
      &quot;src&quot;: &quot;/icons/icon-512.png&quot;,
      &quot;sizes&quot;: &quot;512x512&quot;,
      &quot;type&quot;: &quot;image/png&quot;
    },
    {
      &quot;src&quot;: &quot;/icons/icon-maskable.png&quot;,
      &quot;sizes&quot;: &quot;512x512&quot;,
      &quot;type&quot;: &quot;image/png&quot;,
      &quot;purpose&quot;: &quot;maskable&quot;
    }
  ]
}
</code></pre>
<p>Vincule-o no seu HTML:</p>
<pre><code class="language-html">&lt;link rel=&quot;manifest&quot; href=&quot;/manifest.json&quot;&gt;
&lt;meta name=&quot;theme-color&quot; content=&quot;#4ade80&quot;&gt;
&lt;link rel=&quot;apple-touch-icon&quot; href=&quot;/icons/icon-192.png&quot;&gt;
</code></pre>
<h2>2) Service worker básico</h2>
<p>Crie <code>sw.js</code>:</p>
<pre><code class="language-js">const CACHE_NAME = 'game-v1'

const ASSETS = [
  '/',
  '/index.html',
  '/game.js',
  '/style.css',
  '/assets/sprites.png',
  '/assets/sounds/jump.mp3',
  '/assets/sounds/music.mp3',
]

// Instalação: armazena os recursos em cache
self.addEventListener('install', (event) =&gt; {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) =&gt; {
      return cache.addAll(ASSETS)
    })
  )
})

// Ativação: limpa caches antigos
self.addEventListener('activate', (event) =&gt; {
  event.waitUntil(
    caches.keys().then((keys) =&gt; {
      return Promise.all(
        keys.filter(key =&gt; key !== CACHE_NAME)
            .map(key =&gt; caches.delete(key))
      )
    })
  )
})

// Requisição: fornece o conteúdo do cache e recorre à rede se necessário
self.addEventListener('fetch', (event) =&gt; {
  event.respondWith(
    caches.match(event.request).then((cached) =&gt; {
      return cached || fetch(event.request)
    })
  )
})
</code></pre>
<h2>3) Registre o service worker</h2>
<pre><code class="language-js">if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () =&gt; {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js')
      console.log('SW registered:', registration.scope)
    } catch (err) {
      console.error('SW registration failed:', err)
    }
  })
}
</code></pre>
<h2>4) Cache primeiro, com atualização pela rede</h2>
<p>Melhor para jogos — carregamento rápido com atualizações em segundo plano:</p>
<pre><code class="language-js">self.addEventListener('fetch', (event) =&gt; {
  event.respondWith(
    caches.open(CACHE_NAME).then(async (cache) =&gt; {
      const cached = await cache.match(event.request)
      
      // Inicia a requisição de rede em segundo plano
      const fetchPromise = fetch(event.request).then((response) =&gt; {
        if (response.ok) {
          cache.put(event.request, response.clone())
        }
        return response
      }).catch(() =&gt; null)
      
      // Retorna imediatamente o conteúdo em cache ou aguarda a rede
      return cached || fetchPromise
    })
  )
})
</code></pre>
<h2>5) Cache versionado para atualizações</h2>
<pre><code class="language-js">const CACHE_VERSION = 'v2'
const STATIC_CACHE = `static-${CACHE_VERSION}`
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`

const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/game.js',
  // ... recursos essenciais que raramente mudam
]

self.addEventListener('install', (event) =&gt; {
  self.skipWaiting() // Ativa imediatamente
  
  event.waitUntil(
    caches.open(STATIC_CACHE).then(cache =&gt; cache.addAll(STATIC_ASSETS))
  )
})

self.addEventListener('activate', (event) =&gt; {
  event.waitUntil(
    caches.keys().then(keys =&gt; {
      return Promise.all(
        keys.filter(key =&gt; !key.includes(CACHE_VERSION))
            .map(key =&gt; caches.delete(key))
      )
    })
  )
  
  clients.claim() // Assume o controle imediatamente
})
</code></pre>
<h2>6) Como lidar com atualizações do jogo</h2>
<p>Avise os jogadores quando houver uma atualização disponível:</p>
<pre><code class="language-js">// No aplicativo principal
let refreshing = false

navigator.serviceWorker.addEventListener('controllerchange', () =&gt; {
  if (!refreshing) {
    refreshing = true
    showUpdateNotification()
  }
})

function showUpdateNotification() {
  const banner = document.createElement('div')
  banner.innerHTML = `
    &lt;p&gt;Jogo atualizado! Recarregue para obter a versão mais recente.&lt;/p&gt;
    &lt;button onclick=&quot;location.reload()&quot;&gt;Recarregar&lt;/button&gt;
  `
  banner.className = 'update-banner'
  document.body.appendChild(banner)
}
</code></pre>
<h2>7) Detecção do modo offline</h2>
<pre><code class="language-js">function updateOnlineStatus() {
  if (navigator.onLine) {
    hideOfflineBanner()
    syncGameData()
  } else {
    showOfflineBanner()
  }
}

window.addEventListener('online', updateOnlineStatus)
window.addEventListener('offline', updateOnlineStatus)

function showOfflineBanner() {
  document.getElementById('offline-banner').style.display = 'block'
}

function hideOfflineBanner() {
  document.getElementById('offline-banner').style.display = 'none'
}
</code></pre>
<h2>8) Prompt de instalação</h2>
<pre><code class="language-js">let deferredPrompt = null

window.addEventListener('beforeinstallprompt', (e) =&gt; {
  e.preventDefault()
  deferredPrompt = e
  showInstallButton()
})

function showInstallButton() {
  const btn = document.getElementById('install-btn')
  btn.style.display = 'block'
  btn.addEventListener('click', installApp)
}

async function installApp() {
  if (!deferredPrompt) return
  
  deferredPrompt.prompt()
  const { outcome } = await deferredPrompt.userChoice
  
  console.log('Install prompt outcome:', outcome)
  deferredPrompt = null
  document.getElementById('install-btn').style.display = 'none'
}

window.addEventListener('appinstalled', () =&gt; {
  console.log('App installed!')
  deferredPrompt = null
})
</code></pre>
<p>No iOS e no iPadOS, o evento <code>beforeinstallprompt</code> nunca é disparado, portanto o botão de instalação acima só aparece em navegadores baseados no Chromium. Usuários de iPhone e iPad fazem a instalação tocando em Compartilhar e depois em &quot;Adicionar à Tela de Início&quot;. Uma mudança ocorrida em 2026 que vale a pena conhecer: a partir do Safari 26 (iOS 26 / iPadOS 26), todo site adicionado à Tela de Início é aberto como um aplicativo web por padrão, com a opção &quot;Abrir como Aplicativo Web&quot; ativada por padrão. Não há mais um requisito de instalabilidade no iOS, embora disponibilizar um manifesto e um service worker ainda proporcione uma experiência offline muito melhor.</p>
<h2>9) Sincronização em segundo plano para placares</h2>
<p>Atenção ao suporte: a API Background Sync só está implementada em navegadores baseados no Chromium (Chrome, Edge, Opera e Samsung Internet). O Firefox e o Safari (inclusive no iOS) não oferecem suporte a ela, portanto sempre verifique a disponibilidade do recurso e mantenha uma alternativa que envie as pontuações no próximo carregamento normal do aplicativo.</p>
<pre><code class="language-js">// No service worker
self.addEventListener('sync', (event) =&gt; {
  if (event.tag === 'sync-scores') {
    event.waitUntil(syncScores())
  }
})

async function syncScores() {
  const db = await openDB('game', 1)
  const pendingScores = await db.getAll('pending-scores')
  
  for (const score of pendingScores) {
    try {
      await fetch('/api/scores', {
        method: 'POST',
        body: JSON.stringify(score),
        headers: { 'Content-Type': 'application/json' }
      })
      await db.delete('pending-scores', score.id)
    } catch (err) {
      // Tentará novamente na próxima sincronização
      break
    }
  }
}

// No aplicativo principal
async function submitScore(score) {
  try {
    await fetch('/api/scores', { method: 'POST', body: JSON.stringify(score) })
  } catch {
    // Salva para sincronizar depois
    const db = await openDB('game', 1)
    await db.add('pending-scores', { ...score, id: Date.now() })
    
    if ('serviceWorker' in navigator &amp;&amp; 'sync' in window.registration) {
      await navigator.serviceWorker.ready
      await registration.sync.register('sync-scores')
    }
  }
}
</code></pre>
<h2>10) Testes da PWA</h2>
<p><strong>Chrome DevTools:</strong></p>
<ul>
<li>Application &gt; Service Workers</li>
<li>Application &gt; Manifest</li>
<li>Application &gt; Cache Storage</li>
<li>Network &gt; caixa de seleção Offline</li>
</ul>
<p><strong>Auditoria do Lighthouse:</strong></p>
<ul>
<li>Execute a auditoria de PWA</li>
<li>Verifique a instalabilidade</li>
<li>Verifique a capacidade de funcionar offline</li>
</ul>
<p><strong>Testes em dispositivo real:</strong></p>
<ul>
<li>Instale na tela inicial do celular</li>
<li>Ative o modo avião</li>
<li>Teste todos os recursos offline</li>
</ul>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/service-workers-caching">Service workers para armazenar jogos em cache</a></li>
<li><a href="/pt-BR/tutorials/indexeddb-game-saves">Saves de jogos com IndexedDB</a></li>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carrega rapidamente</a></li>
<li><a href="/pt-BR/tutorials/mobile-friendly-web-games">Jogos web adaptados a dispositivos móveis</a> — controles por toque e gerenciamento da viewport em jogos PWA</li>
<li><a href="/pt-BR/tutorials/streaming-asset-loading">Carregamento de recursos por streaming</a> — estratégias de carregamento que funcionam com o cache do service worker</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps">MDN: Progressive Web Apps</a> — documentação completa sobre PWA</li>
<li><a href="https://web.dev/learn/pwa/">web.dev: Aprenda PWA</a> — trilha de aprendizagem sobre PWA do Google</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Manifest">MDN: Manifesto de Aplicativo Web</a> — referência do arquivo de manifesto</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Canvas de jogo responsivo com dimensionamento correto]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/responsive-game-canvas</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/responsive-game-canvas</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Torne o canvas do seu jogo responsivo: dimensionamento perfeito por pixel, barras de proporção e tratamento de eventos de redimensionamento sem travamentos.]]></description>
            <content:encoded><![CDATA[<h1>Canvas de jogo responsivo</h1>
<p>Um canvas que não é dimensionado corretamente fica borrado ou apresenta problemas em diferentes tamanhos de tela. Já depurei esse problema mais vezes do que consigo contar, então vou poupar você desse trabalho.</p>
<p><strong>Experimente diferentes modos de dimensionamento:</strong></p>
<ResponsiveDemo />
<h2>O problema central</h2>
<p>O canvas tem dois tamanhos que são fáceis de confundir. O tamanho definido pelo CSS controla o tamanho com que ele aparece na tela. A resolução interna controla quantos pixels você realmente desenha. Se esses valores não corresponderem, o resultado será desfoque ou distorção.</p>
<p>Em uma tela Retina, o CSS pode definir 800x600, mas a tela na verdade tem 1600x1200 pixels físicos. Se a resolução interna do canvas for apenas 800x600, o navegador o amplia e tudo fica borrado.</p>
<h2>Configuração responsiva básica</h2>
<p>Veja como fazer um canvas preencher a janela e manter a nitidez:</p>
<pre><code class="language-js">const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')

function resize() {
  const dpr = window.devicePixelRatio || 1
  const width = window.innerWidth
  const height = window.innerHeight
  
  // Define a resolução interna para corresponder aos pixels físicos
  canvas.width = width * dpr
  canvas.height = height * dpr
  
  // Define o tamanho do CSS para corresponder aos pixels lógicos
  canvas.style.width = width + 'px'
  canvas.style.height = height + 'px'
  
  // Dimensiona o contexto para que você possa desenhar em pixels lógicos
  ctx.scale(dpr, dpr)
}

window.addEventListener('resize', resize)
resize()
</code></pre>
<p>A linha <code>ctx.scale(dpr, dpr)</code> é o segredo. Depois disso, você pode desenhar na posição (100, 100), e o conteúdo aparecerá no lugar certo independentemente do DPR. Internamente, o canvas renderiza em 200x200 em uma tela 2x, mas seu código não precisa saber disso.</p>
<p>Uma armadilha: <code>ctx.scale</code> é cumulativo. Se você chamar a função de redimensionamento várias vezes, a escala continuará aumentando. Redefina primeiro a transformação com <code>ctx.setTransform(1, 0, 0, 1, 0, 0)</code> antes de aplicar a escala, ou aplique-a apenas uma vez.</p>
<h2>Proporção fixa com barras</h2>
<p>Muitos jogos precisam de uma proporção específica. Você não quer que seu jogo em 16:9 seja esticado para preencher um monitor ultrawide de 21:9. As barras pretas preservam a proporção.</p>
<pre><code class="language-js">const GAME_WIDTH = 1920
const GAME_HEIGHT = 1080
const ASPECT = GAME_WIDTH / GAME_HEIGHT

function resizeWithLetterbox() {
  const windowWidth = window.innerWidth
  const windowHeight = window.innerHeight
  const windowAspect = windowWidth / windowHeight
  
  let width, height
  
  if (windowAspect &gt; ASPECT) {
    // A janela é mais larga que o jogo; adiciona barras nas laterais
    height = windowHeight
    width = height * ASPECT
  } else {
    // A janela é mais alta que o jogo; adiciona barras em cima e embaixo
    width = windowWidth
    height = width / ASPECT
  }
  
  canvas.style.width = width + 'px'
  canvas.style.height = height + 'px'
  canvas.style.position = 'absolute'
  canvas.style.left = (windowWidth - width) / 2 + 'px'
  canvas.style.top = (windowHeight - height) / 2 + 'px'
  
  // A resolução interna permanece fixa
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
}
</code></pre>
<p>Para que as barras apareçam, defina o fundo do <code>body</code> como preto:</p>
<pre><code class="language-css">body {
  margin: 0;
  background: #000;
  overflow: hidden;
}
</code></pre>
<h2>Escala inteira para pixel art</h2>
<p>Pixel art fica nítida com escalas inteiras (1x, 2x, 3x) e borrada com escalas fracionárias, como 2,7x. Mesmo com <code>image-rendering: pixelated</code>, uma escala de 2,5x significa que alguns pixels terão 2 pixels de tela de largura e outros terão 3.</p>
<pre><code class="language-js">const GAME_WIDTH = 320
const GAME_HEIGHT = 180

function resizePixelPerfect() {
  const maxWidth = window.innerWidth
  const maxHeight = window.innerHeight
  
  // Encontra a maior escala inteira que cabe na tela
  const scaleX = Math.floor(maxWidth / GAME_WIDTH)
  const scaleY = Math.floor(maxHeight / GAME_HEIGHT)
  const scale = Math.max(1, Math.min(scaleX, scaleY))
  
  const width = GAME_WIDTH * scale
  const height = GAME_HEIGHT * scale
  
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
  canvas.style.width = width + 'px'
  canvas.style.height = height + 'px'
  canvas.style.imageRendering = 'pixelated'
}
</code></pre>
<p>A desvantagem é que o jogo não preencherá a tela por completo. Em uma tela de 1920x1080 com um jogo de 320x180, você obtém uma escala de 5x (1600x900), com barras pretas ao redor das bordas. Alguns jogadores preferem isso; outros querem que o jogo preencha a tela, mesmo que fique um pouco borrado.</p>
<h2>Dimensionamento do canvas WebGL</h2>
<p>No WebGL, você precisa atualizar a viewport além do tamanho do canvas:</p>
<pre><code class="language-js">function resizeGL(gl, canvas) {
  const dpr = window.devicePixelRatio || 1
  const width = canvas.clientWidth * dpr
  const height = canvas.clientHeight * dpr
  
  if (canvas.width !== width || canvas.height !== height) {
    canvas.width = width
    canvas.height = height
    gl.viewport(0, 0, width, height)
    return true // Redimensionado; talvez seja necessário atualizar as matrizes de projeção
  }
  return false
}
</code></pre>
<p>Chame essa função no início do loop de renderização, não apenas em eventos de redimensionamento. Alguns navegadores alteram o DPR quando você arrasta uma janela entre monitores, e é importante detectar essa mudança.</p>
<h2>Aplicando debounce aos eventos de redimensionamento</h2>
<p>Os eventos de redimensionamento são disparados rapidamente enquanto o usuário arrasta a borda da janela. Se sua função de redimensionamento for pesada, ocorrerão travamentos.</p>
<pre><code class="language-js">let resizeTimeout
window.addEventListener('resize', () =&gt; {
  clearTimeout(resizeTimeout)
  resizeTimeout = setTimeout(resize, 100)
})
</code></pre>
<p>O atraso de 100 ms significa que o redimensionamento só acontece depois que o usuário para de arrastar. Para um tratamento mais suave durante o arraste, use ResizeObserver:</p>
<pre><code class="language-js">const observer = new ResizeObserver(entries =&gt; {
  resize()
})
observer.observe(document.body)
</code></pre>
<p>ResizeObserver é acionado com menos frequência e tem uma otimização melhor pelo navegador.</p>
<h2>Tela cheia</h2>
<p>A API Fullscreen permite ocupar a tela inteira:</p>
<pre><code class="language-js">async function toggleFullscreen() {
  if (!document.fullscreenElement) {
    await canvas.requestFullscreen()
  } else {
    await document.exitFullscreen()
  }
}

document.addEventListener('fullscreenchange', resize)
</code></pre>
<p>Você deve chamar <code>requestFullscreen</code> em resposta a um gesto do usuário, como um clique. Caso contrário, os navegadores bloqueiam a solicitação. E lembre-se de redimensionar ao entrar ou sair do modo de tela cheia, pois o espaço disponível muda.</p>
<p>Uma ressalva de plataforma: em meados de 2026, a API Fullscreen ainda não oferece tela cheia para elementos no Safari do iPhone. <code>requestFullscreen</code> funciona em navegadores para desktop e no iPadOS, mas não é compatível no iPhone com elementos arbitrários, como um canvas — apenas a reprodução nativa de vídeo em tela cheia funciona. Detecte a disponibilidade do recurso com <code>if (canvas.requestFullscreen)</code> e ofereça uma alternativa adequada, como dimensionar o canvas para preencher a viewport visível ou sugerir que os usuários adicionem o jogo à tela inicial como um PWA.</p>
<h2>Juntando tudo</h2>
<p>Veja uma classe que lida com todos os casos comuns:</p>
<pre><code class="language-js">class ResponsiveCanvas {
  constructor(canvas, options = {}) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.gameWidth = options.width || 1280
    this.gameHeight = options.height || 720
    this.pixelArt = options.pixelArt || false
    this.letterbox = options.letterbox !== false
    
    this.bind()
    this.resize()
  }
  
  bind() {
    window.addEventListener('resize', () =&gt; this.resize())
    document.addEventListener('fullscreenchange', () =&gt; this.resize())
  }
  
  resize() {
    const aspect = this.gameWidth / this.gameHeight
    let width = window.innerWidth
    let height = window.innerHeight
    
    if (this.letterbox) {
      if (width / height &gt; aspect) {
        width = height * aspect
      } else {
        height = width / aspect
      }
    }
    
    if (this.pixelArt) {
      const scale = Math.max(1, Math.floor(Math.min(
        window.innerWidth / this.gameWidth,
        window.innerHeight / this.gameHeight
      )))
      width = this.gameWidth * scale
      height = this.gameHeight * scale
    }
    
    this.canvas.width = this.gameWidth
    this.canvas.height = this.gameHeight
    this.canvas.style.width = width + 'px'
    this.canvas.style.height = height + 'px'
    this.canvas.style.position = 'absolute'
    this.canvas.style.left = (window.innerWidth - width) / 2 + 'px'
    this.canvas.style.top = (window.innerHeight - height) / 2 + 'px'
    
    if (this.pixelArt) {
      this.canvas.style.imageRendering = 'pixelated'
    }
  }
  
  get width() { return this.gameWidth }
  get height() { return this.gameHeight }
}
</code></pre>
<p>Use assim:</p>
<pre><code class="language-js">const responsive = new ResponsiveCanvas(document.getElementById('game'), {
  width: 320,
  height: 180,
  pixelArt: true,
  letterbox: true
})

// No código do jogo, use responsive.width e responsive.height
</code></pre>
<h2>Armadilhas comuns</h2>
<p>Alguns detalhes que costumam causar problemas:</p>
<p><strong>O canvas é apagado ao ser redimensionado.</strong> Quando você define <code>canvas.width</code> ou <code>canvas.height</code>, o canvas é apagado. Se você não redesenha a cada frame, precisa redesenhar após o redimensionamento.</p>
<p><strong>O DPR pode mudar.</strong> Se o usuário arrastar a janela do navegador de uma tela Retina para uma tela que não seja Retina, o DPR mudará. Verifique-o no loop de renderização, não apenas ao redimensionar.</p>
<p><strong>Peculiaridades da viewport no Safari do iOS.</strong> A altura da viewport muda quando a barra de endereço aparece ou desaparece. Use <code>window.visualViewport</code> para obter dimensões mais precisas em dispositivos móveis.</p>
<p><strong>O estado do contexto é redefinido.</strong> Definir as dimensões do canvas redefine o estado do contexto, incluindo transformações, estilos de preenchimento e <code>imageSmoothingEnabled</code>. Reaplique essas configurações após o redimensionamento.</p>
<h2>Mais recursos</h2>
<p><a href="/pt-BR/tutorials/canvas-2d-game-loop">Loop de jogo com Canvas 2D</a> aborda os fundamentos dos loops de atualização e renderização.</p>
<p><a href="/pt-BR/tutorials/pixel-art-rendering">Renderização de pixel art</a> se aprofunda em gráficos de pixels nítidos.</p>
<p><a href="/pt-BR/tutorials/mobile-friendly-web-games">Jogos web adaptados para dispositivos móveis</a> aborda controles por toque e problemas de viewport.</p>
<p><a href="/pt-BR/tutorials/webgl-fundamentals-for-games">Fundamentos de WebGL</a> aborda a configuração da viewport e do canvas para contextos WebGL.</p>
<p><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rapidamente</a> aborda a otimização do tamanho do canvas para melhorar o desempenho.</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Service workers para cache de recursos de jogos]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/service-workers-caching</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/service-workers-caching</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Armazene recursos de jogos em cache com service workers: estratégias, versionamento e atualizações. Faça seu jogo carregar instantaneamente em visitas recorrentes.]]></description>
            <content:encoded><![CDATA[<h1>Service workers para cache de recursos de jogos</h1>
<p>Os service workers permitem controlar como os recursos são armazenados em cache e entregues. Para jogos, isso significa carregamento instantâneo em visitas recorrentes e suporte offline.</p>
<h2>1) Noções básicas de service workers</h2>
<p>Crie <code>sw.js</code> na raiz:</p>
<pre><code class="language-js">// sw.js
self.addEventListener('install', (event) =&gt; {
  console.log('Service worker installing')
})

self.addEventListener('activate', (event) =&gt; {
  console.log('Service worker activated')
})

self.addEventListener('fetch', (event) =&gt; {
  // Intercept all network requests
})
</code></pre>
<p>Registre-o:</p>
<pre><code class="language-js">if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg =&gt; console.log('SW registered'))
    .catch(err =&gt; console.error('SW registration failed:', err))
}
</code></pre>
<h2>2) Estratégia cache-first (a melhor para recursos de jogos)</h2>
<p>Carregue do cache e só busque na rede se o recurso não estiver armazenado:</p>
<pre><code class="language-js">const CACHE_NAME = 'game-assets-v1'

self.addEventListener('fetch', (event) =&gt; {
  event.respondWith(
    caches.match(event.request).then((cached) =&gt; {
      if (cached) {
        return cached
      }
      
      return fetch(event.request).then((response) =&gt; {
        // Don't cache non-ok responses or non-GET requests
        if (!response.ok || event.request.method !== 'GET') {
          return response
        }
        
        // Clone response for caching
        const clone = response.clone()
        caches.open(CACHE_NAME).then((cache) =&gt; {
          cache.put(event.request, clone)
        })
        
        return response
      })
    })
  )
})
</code></pre>
<h2>3) Pré-armazenamento de recursos essenciais</h2>
<p>O evento de instalação é perfeito para o pré-armazenamento em cache:</p>
<pre><code class="language-js">const CACHE_NAME = 'game-v1'

const PRECACHE = [
  '/',
  '/index.html',
  '/game.js',
  '/style.css',
  '/assets/sprites.png',
  '/assets/tileset.png',
  '/assets/player.png',
]

self.addEventListener('install', (event) =&gt; {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache =&gt; cache.addAll(PRECACHE))
      .then(() =&gt; self.skipWaiting())
  )
})
</code></pre>
<h2>4) Network-first para conteúdo dinâmico</h2>
<p>Use network-first para itens que mudam (rankings, dados de jogadores):</p>
<pre><code class="language-js">self.addEventListener('fetch', (event) =&gt; {
  const url = new URL(event.request.url)
  
  // API calls: network first
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(event.request))
    return
  }
  
  // Assets: cache first
  event.respondWith(cacheFirst(event.request))
})

async function networkFirst(request) {
  try {
    const response = await fetch(request)
    const cache = await caches.open('api-cache')
    cache.put(request, response.clone())
    return response
  } catch {
    return caches.match(request)
  }
}

async function cacheFirst(request) {
  const cached = await caches.match(request)
  if (cached) return cached
  
  const response = await fetch(request)
  const cache = await caches.open(CACHE_NAME)
  cache.put(request, response.clone())
  return response
}
</code></pre>
<h2>5) Stale-while-revalidate</h2>
<p>Retorne o conteúdo armazenado em cache imediatamente e atualize-o em segundo plano:</p>
<pre><code class="language-js">async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME)
  const cached = await cache.match(request)
  
  const fetchPromise = fetch(request).then((response) =&gt; {
    if (response.ok) {
      cache.put(request, response.clone())
    }
    return response
  })
  
  return cached || fetchPromise
}
</code></pre>
<h2>6) Versionamento e limpeza do cache</h2>
<p>Remova caches antigos durante a ativação:</p>
<pre><code class="language-js">const CACHE_VERSION = 'v2'
const CACHE_NAME = `game-${CACHE_VERSION}`

self.addEventListener('activate', (event) =&gt; {
  event.waitUntil(
    caches.keys().then((keys) =&gt; {
      return Promise.all(
        keys
          .filter(key =&gt; key.startsWith('game-') &amp;&amp; key !== CACHE_NAME)
          .map(key =&gt; {
            console.log('Deleting old cache:', key)
            return caches.delete(key)
          })
      )
    }).then(() =&gt; self.clients.claim())
  )
})
</code></pre>
<h2>7) Caches separados por tipo</h2>
<p>Organize os caches para facilitar o gerenciamento:</p>
<pre><code class="language-js">const CACHES = {
  static: 'static-v1',    // HTML, JS, CSS
  images: 'images-v1',    // Sprites, textures
  audio: 'audio-v1',      // Music, SFX
  api: 'api-v1',          // API responses
}

self.addEventListener('fetch', (event) =&gt; {
  const url = new URL(event.request.url)
  
  if (url.pathname.match(/\.(png|jpg|webp)$/)) {
    event.respondWith(cacheFirst(event.request, CACHES.images))
  } else if (url.pathname.match(/\.(mp3|ogg|wav)$/)) {
    event.respondWith(cacheFirst(event.request, CACHES.audio))
  } else if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(event.request, CACHES.api))
  } else {
    event.respondWith(cacheFirst(event.request, CACHES.static))
  }
})
</code></pre>
<h2>8) Solicitações de intervalo para áudio e vídeo</h2>
<p>Uma coisa que você precisa saber desde o início: a Cache API não armazena respostas parciais. Se uma resposta 206 Partial Content chegar a cache.put(), será lançado TypeError: Failed to execute 'put' on 'Cache': Partial response (status code 206) is unsupported. Portanto, o padrão para mídia armazenada em cache é sempre o mesmo: pré-armazenar (ou buscar) o arquivo completo como uma resposta 200 normal e, em seguida, criar a resposta 206 fazendo o recorte dos bytes armazenados em cache sempre que uma solicitação chegar com um cabeçalho Range. Se preferir não implementar isso manualmente, o plugin range-requests do Workbox faz exatamente esse recorte quando combinado com uma estratégia CacheFirst.</p>
<p>Processe corretamente as solicitações de intervalo (necessárias para avançar ou retroceder no áudio):</p>
<pre><code class="language-js">self.addEventListener('fetch', (event) =&gt; {
  if (event.request.headers.has('range')) {
    event.respondWith(handleRangeRequest(event.request))
    return
  }
  // ... normal handling
})

async function handleRangeRequest(request) {
  const cache = await caches.open(CACHES.audio)
  // Match the full resource, ignoring the Range header (which would never match a cached full file)
  let cached = await cache.match(request.url)

  if (!cached) {
    // Fetch the FULL file (string URL drops the Range header) and cache it as a 200
    cached = await fetch(request.url)
    if (cached.ok) cache.put(request.url, cached.clone()) // cache.put rejects 206, so only store full 200s
  }

  // Build a 206 Partial Content response from the cached full body
  const rangeHeader = request.headers.get('range') // e.g. &quot;bytes=200-1000&quot;
  const buffer = await cached.arrayBuffer()
  const total = buffer.byteLength
  const [startStr, endStr] = rangeHeader.replace(/bytes=/, '').split('-')
  const start = Number(startStr)
  const end = endStr ? Number(endStr) : total - 1
  const slice = buffer.slice(start, end + 1)

  return new Response(slice, {
    status: 206,
    statusText: 'Partial Content',
    headers: {
      'Content-Type': cached.headers.get('Content-Type') || 'application/octet-stream',
      'Content-Range': `bytes ${start}-${end}/${total}`,
      'Content-Length': String(slice.byteLength),
      'Accept-Ranges': 'bytes',
    },
  })
}
</code></pre>
<h2>9) Notificação de atualização</h2>
<p>Avise aos jogadores quando uma nova versão estiver pronta:</p>
<pre><code class="language-js">// In main app
let refreshing = false

navigator.serviceWorker.addEventListener('controllerchange', () =&gt; {
  if (refreshing) return
  refreshing = true
  
  showUpdateBanner()
})

function showUpdateBanner() {
  const banner = document.createElement('div')
  banner.className = 'update-banner'
  banner.innerHTML = `
    &lt;span&gt;New version available!&lt;/span&gt;
    &lt;button onclick=&quot;location.reload()&quot;&gt;Update&lt;/button&gt;
  `
  document.body.appendChild(banner)
}
</code></pre>
<h2>10) Service worker completo para jogos</h2>
<pre><code class="language-js">const VERSION = 'v1'
const STATIC_CACHE = `static-${VERSION}`
const ASSET_CACHE = `assets-${VERSION}`

const PRECACHE_URLS = [
  '/',
  '/index.html',
  '/game.js',
  '/style.css',
]

// Install: precache critical resources
self.addEventListener('install', (event) =&gt; {
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then(cache =&gt; cache.addAll(PRECACHE_URLS))
      .then(() =&gt; self.skipWaiting())
  )
})

// Activate: clean old caches
self.addEventListener('activate', (event) =&gt; {
  event.waitUntil(
    caches.keys().then(keys =&gt; Promise.all(
      keys
        .filter(key =&gt; !key.endsWith(VERSION))
        .map(key =&gt; caches.delete(key))
    )).then(() =&gt; self.clients.claim())
  )
})

// Fetch: route requests to appropriate strategy
self.addEventListener('fetch', (event) =&gt; {
  const url = new URL(event.request.url)
  
  // Same-origin only
  if (url.origin !== location.origin) {
    return
  }
  
  // API: network first with cache fallback
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(event.request, 'api-cache'))
    return
  }
  
  // Assets: cache first
  if (url.pathname.startsWith('/assets/')) {
    event.respondWith(cacheFirst(event.request, ASSET_CACHE))
    return
  }
  
  // HTML/JS/CSS: stale while revalidate
  event.respondWith(staleWhileRevalidate(event.request, STATIC_CACHE))
})

async function cacheFirst(request, cacheName) {
  const cached = await caches.match(request)
  if (cached) return cached
  
  try {
    const response = await fetch(request)
    if (response.ok) {
      const cache = await caches.open(cacheName)
      cache.put(request, response.clone())
    }
    return response
  } catch {
    return new Response('Offline', { status: 503 })
  }
}

async function networkFirst(request, cacheName) {
  try {
    const response = await fetch(request)
    if (response.ok) {
      const cache = await caches.open(cacheName)
      cache.put(request, response.clone())
    }
    return response
  } catch {
    return caches.match(request) || new Response('Offline', { status: 503 })
  }
}

async function staleWhileRevalidate(request, cacheName) {
  const cache = await caches.open(cacheName)
  const cached = await cache.match(request)
  
  const fetchPromise = fetch(request).then(response =&gt; {
    if (response.ok) {
      cache.put(request, response.clone())
    }
    return response
  }).catch(() =&gt; cached)
  
  return cached || fetchPromise
}
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/pwa-offline-games">PWA para jogos offline</a></li>
<li><a href="/pt-BR/tutorials/streaming-asset-loading">Carregamento de recursos por streaming</a></li>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carrega rapidamente</a></li>
<li><a href="/pt-BR/tutorials/web-audio-api-games">Web Audio API para jogos</a> — armazenamento de recursos de áudio em cache para jogar offline</li>
<li><a href="/pt-BR/tutorials/indexeddb-game-saves">Saves de jogos com IndexedDB</a> — persistência do estado do jogo junto aos recursos armazenados em cache</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">MDN: API Service Worker</a> — referência completa da API</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Cache">MDN: API Cache</a> — referência da interface Cache</li>
<li><a href="https://web.dev/learn/pwa/service-workers/">web.dev: Service workers</a> — estratégias e padrões de armazenamento em cache</li>
<li><a href="https://developer.chrome.com/docs/workbox/">Workbox</a> — biblioteca do Google com ferramentas para service workers</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Publique um jogo para web que carregue rápido (um checklist prático)]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/ship-web-game-fast</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/ship-web-game-fast</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Um checklist pragmático e passo a passo para fazer jogos para web carregarem rapidamente: limites, compressão, cache, formatos de recursos e medições de desempenho.]]></description>
            <content:encoded><![CDATA[<h1>Publique um jogo para web que carregue rápido (um checklist prático)</h1>
<p>Se os jogadores não conseguem <strong>começar a jogar em poucos segundos</strong>, eles desistem. Este tutorial é um checklist prático que você pode aplicar a praticamente qualquer stack de jogos para web (Canvas/WebGL/WebGPU, Pixi/Three/PlayCanvas, engine própria etc.).</p>
<h2>1) Defina os limites primeiro (ou você não conseguirá otimizar)</h2>
<p>Escolha limites adequados ao dispositivo-alvo. Uma boa configuração padrão para uma experiência de “jogar na hora”:</p>
<ul>
<li><strong>JS+CSS enviados</strong>: menos de ~300–500 KB com gzip (quanto menor, melhor)</li>
<li><strong>Recursos críticos antes da primeira interação</strong>: menos de ~2–5 MB compactados</li>
<li><strong>Tempo até a primeira interação</strong>: menos de ~3–5 segundos em um celular intermediário</li>
</ul>
<p>Anote esses limites e trate-os como contratos de API.</p>
<h2>2) Meça as coisas certas</h2>
<p>No Chrome DevTools:</p>
<ul>
<li>Use <strong>Performance</strong> para capturar uma sessão e inspecionar tarefas longas.</li>
<li>Use <strong>Network</strong> para verificar:
<ul>
<li>tamanho transferido em comparação com o tamanho decodificado</li>
<li>cabeçalhos de cache</li>
<li>se os recursos são transmitidos progressivamente ou chegam como um único arquivo enorme</li>
</ul>
</li>
</ul>
<p>Em termos de jogo, acompanhe:</p>
<ul>
<li>Tempo até “<strong>O jogador consegue se mover?</strong>”</li>
<li>Tempo até o “<strong>Primeiro frame significativo</strong>”</li>
<li>“<strong>Latência da primeira interação</strong>” (interação → frame)</li>
</ul>
<p>Sua noção de “latência da primeira interação” agora corresponde a uma métrica padrão: a <strong>Interaction to Next Paint (INP)</strong> substituiu a First Input Delay como uma das Core Web Vitals em março de 2024 e mede o tempo completo entre a interação e a próxima renderização em todas as interações, não apenas na primeira. Procure manter a INP abaixo de 200 ms no 75º percentil, uma referência externa útil para os números de responsividade que você já acompanha.</p>
<h2>3) Comprima tudo o que puder</h2>
<p>Certifique-se de que sua hospedagem forneça:</p>
<ul>
<li><strong>Brotli</strong> (<code>br</code>) para recursos de texto (JS/CSS/HTML/WASM), quando disponível</li>
<li><strong>gzip</strong> como alternativa</li>
</ul>
<p>Se sua hospedagem e CDN oferecerem suporte, <strong>Zstandard</strong> (<code>zstd</code>) agora é uma terceira opção para <code>Content-Encoding</code>. Ele está disponível no Chrome e Edge 123+, Firefox 126+ e Safari 26.3+ (cerca de 80% dos usuários) e pode alcançar taxas de compressão comparáveis às do Brotli com um custo de CPU menor no servidor, o que ajuda com arquivos WASM e JSON grandes. Os navegadores só anunciam <code>Accept-Encoding: zstd</code> por HTTPS, portanto mantenha Brotli e gzip configurados como alternativas para todos os demais.</p>
<p>Em builds com WebAssembly, a compressão muitas vezes faz a diferença entre “instantâneo” e “nunca carrega”.</p>
<h2>4) Não baixe o mundo inteiro logo no início</h2>
<p>Divida seu jogo em:</p>
<ul>
<li><strong>Inicialização</strong>: carregador mínimo + entrada + primeira cena</li>
<li><strong>Núcleo</strong>: principais sistemas de jogabilidade</li>
<li><strong>Opcional</strong>: fases extras, itens cosméticos, texturas em alta resolução, áudio bônus etc.</li>
</ul>
<p>Carregue o conteúdo “Opcional” somente depois que:</p>
<ul>
<li>o jogador já puder jogar e/ou</li>
<li>você souber que ele quer esse conteúdo (por exemplo, porque chegou a um menu ou fase)</li>
</ul>
<h2>5) Escolha os formatos de recursos certos</h2>
<p>Para a maioria dos jogos para web:</p>
<ul>
<li><strong>Imagens</strong>: prefira <strong>WebP</strong> (ou <strong>AVIF</strong>, se puder tolerar uma codificação mais lenta) para interfaces e planos de fundo.</li>
<li><strong>Áudio</strong>: prefira <strong>Ogg Vorbis</strong> para uso geral; teste <strong>AAC</strong> nos ecossistemas da Apple.</li>
<li><strong>Vídeo</strong>: prefira <strong>MP4/H.264</strong> pela compatibilidade e <strong>WebM</strong> quando houver suporte e o arquivo for menor.</li>
</ul>
<p>Fique atento ao custo de decodificação: um “download menor” ainda pode ser “mais lento para decodificar”.</p>
<h2>6) Mantenha sua primeira cena pequena e determinística</h2>
<p>Evite o seguinte em sua primeira cena interativa:</p>
<ul>
<li>picos de compilação de shaders</li>
<li>uploads de texturas enormes</li>
<li>geração procedural que bloqueia a thread principal</li>
<li>análise síncrona de blocos JSON com vários megabytes</li>
</ul>
<p>Especificamente para texturas, distribua <strong>KTX2 com supercompressão Basis Universal</strong> em vez de PNG ou JPEG. A textura permanece compactada na GPU após o upload (sendo transcodificada para BC7 em desktops ou ASTC em dispositivos móveis durante o carregamento), o que reduz o uso de VRAM de 4 a 8 vezes e torna os uploads muito menos custosos do que decodificar um PNG em tamanho completo e enviar os pixels brutos para a GPU.</p>
<p>Se precisar executar um processamento pesado, faça isso:</p>
<ul>
<li>de forma incremental ao longo dos frames ou</li>
<li>em um Worker (quando possível)</li>
</ul>
<h2>7) Use cache de forma agressiva (mas corretamente)</h2>
<p>Use cache de longa duração para recursos endereçados por conteúdo:</p>
<ul>
<li><code>Cache-Control: public, max-age=31536000, immutable</code></li>
</ul>
<p>Para os pontos de entrada HTML, mantenha o cache curto para poder publicar atualizações com segurança.</p>
<h2>8) Torne as “falhas” rápidas e compreensíveis</h2>
<p>Se algo der errado, exiba:</p>
<ul>
<li>uma mensagem de erro clara</li>
<li>um botão para tentar novamente</li>
<li>um link para relatar o problema</li>
</ul>
<p>Falhas silenciosas acabam com a confiança.</p>
<h2>9) Se você usa threads em Wasm, entenda COOP/COEP</h2>
<p>Se sua build depende de <code>SharedArrayBuffer</code> (algo comum em algumas configurações de threading com Wasm), provavelmente serão necessários cabeçalhos de <strong>isolamento entre origens</strong>:</p>
<ul>
<li><code>Cross-Origin-Opener-Policy: same-origin</code></li>
<li><code>Cross-Origin-Embedder-Policy: require-corp</code></li>
</ul>
<p>Você tem duas opções para o cabeçalho COEP. <code>require-corp</code> é a opção mais rigorosa, mas exige que todos os recursos de outra origem (texturas de CDN e scripts de terceiros) enviem um cabeçalho <code>Cross-Origin-Resource-Policy</code>; caso contrário, eles serão bloqueados. <code>Cross-Origin-Embedder-Policy: credentialless</code> também libera o <code>SharedArrayBuffer</code> e o isolamento entre origens, mas carrega recursos de outras origens sem credenciais em vez de exigir que eles aceitem isso por meio de CORP. Se você obtém recursos de uma CDN que não controla, <code>credentialless</code> geralmente é o caminho menos trabalhoso.</p>
<p>Faça testes desde o início em sua hospedagem real, pois os cabeçalhos e conteúdos incorporados de terceiros podem causar problemas.</p>
<h2>10) Torne o jogo executável sem um SDK</h2>
<p>Se você pretende usar o fluxo de trabalho para criadores da Cinevva, procure oferecer:</p>
<ul>
<li>uma única URL que inicialize o jogo de forma confiável</li>
<li>nenhum login obrigatório para iniciar uma demonstração</li>
<li>controles de entrada previsíveis</li>
</ul>
<p>Assim, você estará pronto para distribuir seu jogo sem integrar SDKs adicionais.</p>
<p>Relacionado:</p>
<ul>
<li><a href="/pt-BR/creators">Para criadores de jogos</a></li>
<li><a href="/pt-BR/ai-content">Política sobre conteúdo gerado por IA</a></li>
<li><a href="/pt-BR/tutorials/streaming-asset-loading">Carregamento de recursos por streaming</a> — padrões de carregamento progressivo para jogos grandes</li>
<li><a href="/pt-BR/tutorials/service-workers-caching">Service workers para cache de jogos</a> — armazene recursos em cache para visitas repetidas instantâneas</li>
<li><a href="/pt-BR/guides/web-game-engines-comparison">Comparação de engines de jogos para web</a> — tamanhos de build e tempos de carregamento entre diferentes engines</li>
<li><a href="/pt-BR/guides/itch-io-launch-guide">Como lançar seu jogo no itch.io</a> — otimização de jogos para navegador incorporados ao <a href="http://itch.io">itch.io</a></li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Carregamento progressivo de assets para jogos web]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/streaming-asset-loading</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/streaming-asset-loading</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Carregue assets do jogo progressivamente: priorização, streaming, cache e exibição do progresso. Mantenha os jogadores engajados durante o carregamento.]]></description>
            <content:encoded><![CDATA[<h1>Carregamento progressivo de assets para jogos web</h1>
<p>Os jogadores não vão esperar por um download de 100 MB. Carregue os assets progressivamente para que possam jogar enquanto o restante é carregado em segundo plano.</p>
<h2>1) A estratégia de carregamento</h2>
<p>Divida os assets em categorias:</p>
<ol>
<li><strong>Críticos</strong> — Necessários para exibir a primeira tela (&lt; 1 MB)</li>
<li><strong>Jogabilidade</strong> — Necessários para jogar (&lt; 10 MB)</li>
<li><strong>Aprimoramentos</strong> — Opcionais (carregados em segundo plano)</li>
</ol>
<h2>2) Carregador básico de assets</h2>
<pre><code class="language-js">class AssetLoader {
  constructor() {
    this.cache = new Map()
    this.loading = new Map()
  }
  
  async loadImage(url) {
    if (this.cache.has(url)) return this.cache.get(url)
    if (this.loading.has(url)) return this.loading.get(url)
    
    const promise = new Promise((resolve, reject) =&gt; {
      const img = new Image()
      img.onload = () =&gt; {
        this.cache.set(url, img)
        this.loading.delete(url)
        resolve(img)
      }
      img.onerror = reject
      img.src = url
    })
    
    this.loading.set(url, promise)
    return promise
  }
  
  async loadJSON(url) {
    if (this.cache.has(url)) return this.cache.get(url)
    
    const response = await fetch(url)
    const data = await response.json()
    this.cache.set(url, data)
    return data
  }
  
  async loadAudio(url, audioCtx) {
    if (this.cache.has(url)) return this.cache.get(url)
    
    const response = await fetch(url)
    const buffer = await response.arrayBuffer()
    const audioBuffer = await audioCtx.decodeAudioData(buffer)
    this.cache.set(url, audioBuffer)
    return audioBuffer
  }
}
</code></pre>
<h2>3) Carregamento com progresso</h2>
<pre><code class="language-js">async function loadWithProgress(url, onProgress) {
  const response = await fetch(url)
  const contentLength = response.headers.get('Content-Length')
  const total = parseInt(contentLength, 10)
  
  const reader = response.body.getReader()
  const chunks = []
  let received = 0
  
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    
    chunks.push(value)
    received += value.length
    onProgress(received / total)
  }
  
  const blob = new Blob(chunks)
  return blob
}
</code></pre>
<p>Atenção: quando o servidor comprime a resposta (com gzip ou brotli, como a maioria das CDNs faz por padrão), <code>Content-Length</code> representa o tamanho compactado, enquanto os blocos que você lê já estão descompactados. Isso faz com que <code>received / total</code> ultrapasse 100%. Para obter uma barra de progresso precisa em nível de bytes para respostas compactadas, sirva o asset sem compactação, envie um cabeçalho próprio com o tamanho descompactado ou use como alternativa o evento <code>onprogress</code> do <code>XMLHttpRequest</code>, que informa o progresso real da transferência.</p>
<h2>4) Carregamento em lote com progresso geral</h2>
<pre><code class="language-js">async function loadAssets(manifest, onProgress) {
  const total = manifest.length
  let completed = 0
  const results = {}
  
  const promises = manifest.map(async (item) =&gt; {
    const asset = await loadAsset(item.url, item.type)
    results[item.name] = asset
    completed++
    onProgress(completed / total, item.name)
  })
  
  await Promise.all(promises)
  return results
}

// Uso
const manifest = [
  { name: 'player', url: 'player.png', type: 'image' },
  { name: 'level1', url: 'level1.json', type: 'json' },
  { name: 'music', url: 'music.mp3', type: 'audio' },
]

const assets = await loadAssets(manifest, (progress, name) =&gt; {
  console.log(`Carregando: ${Math.round(progress * 100)}% (${name})`)
})
</code></pre>
<h2>5) Carregador com fila de prioridade</h2>
<pre><code class="language-js">class PriorityLoader {
  constructor(concurrency = 4) {
    this.queue = []
    this.active = 0
    this.concurrency = concurrency
  }
  
  add(url, priority = 0) {
    return new Promise((resolve, reject) =&gt; {
      this.queue.push({ url, priority, resolve, reject })
      this.queue.sort((a, b) =&gt; b.priority - a.priority)
      this.process()
    })
  }
  
  async process() {
    if (this.active &gt;= this.concurrency || this.queue.length === 0) return
    
    this.active++
    const { url, resolve, reject } = this.queue.shift()
    
    try {
      const response = await fetch(url)
      const blob = await response.blob()
      resolve(blob)
    } catch (err) {
      reject(err)
    }
    
    this.active--
    this.process()
  }
}

// Uso
const loader = new PriorityLoader()
loader.add('critical.png', 10)  // Carregar primeiro
loader.add('optional.png', 1)   // Carregar depois
</code></pre>
<h2>6) Carregamento sob demanda para fases</h2>
<pre><code class="language-js">class LevelManager {
  constructor(loader) {
    this.loader = loader
    this.levels = new Map()
  }
  
  async preload(levelId) {
    if (this.levels.has(levelId)) return
    
    const manifest = await this.loader.loadJSON(`levels/${levelId}/manifest.json`)
    const assets = await this.loadLevelAssets(manifest)
    this.levels.set(levelId, { manifest, assets })
  }
  
  async loadLevelAssets(manifest) {
    // Carregar somente o necessário para esta fase
    const assets = {}
    
    for (const texture of manifest.textures) {
      assets[texture.name] = await this.loader.loadImage(texture.url)
    }
    
    return assets
  }
  
  unload(levelId) {
    this.levels.delete(levelId)
    // Os assets podem ser coletados pelo garbage collector
  }
}
</code></pre>
<h2>7) Streaming de arquivos grandes</h2>
<p>Para arquivos grandes (modelos 3D, áudio), faça o streaming e processe-os de forma incremental:</p>
<pre><code class="language-js">async function streamLargeFile(url, onChunk) {
  const response = await fetch(url)
  const reader = response.body.getReader()
  
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    onChunk(value)
  }
}

// Para áudio: use Media Source Extensions
// Para 3D: processe os dados da malha conforme chegam
</code></pre>
<h2>8) Cache dos assets carregados</h2>
<p>Combine com o IndexedDB para ter um cache persistente:</p>
<pre><code class="language-js">class CachedLoader {
  constructor() {
    this.memCache = new Map()
    this.dbName = 'AssetCache'
  }
  
  async loadImage(url) {
    // Verificar a memória
    if (this.memCache.has(url)) return this.memCache.get(url)
    
    // Verificar o IndexedDB
    const cached = await this.getFromDB(url)
    if (cached) {
      const img = await this.blobToImage(cached)
      this.memCache.set(url, img)
      return img
    }
    
    // Buscar e armazenar em cache
    const response = await fetch(url)
    const blob = await response.blob()
    await this.saveToDB(url, blob)
    
    const img = await this.blobToImage(blob)
    this.memCache.set(url, img)
    return img
  }
  
  blobToImage(blob) {
    return new Promise((resolve) =&gt; {
      const img = new Image()
      img.onload = () =&gt; {
        URL.revokeObjectURL(img.src)
        resolve(img)
      }
      img.src = URL.createObjectURL(blob)
    })
  }
  
  // Métodos do IndexedDB...
}
</code></pre>
<h2>9) Padrão de tela de carregamento</h2>
<pre><code class="language-js">class LoadingScreen {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.progress = 0
    this.message = 'Carregando...'
  }
  
  update(progress, message) {
    this.progress = progress
    this.message = message || this.message
    this.render()
  }
  
  render() {
    const { ctx, canvas } = this
    ctx.fillStyle = '#1a1a2e'
    ctx.fillRect(0, 0, canvas.width, canvas.height)
    
    // Barra de progresso
    const barWidth = canvas.width * 0.6
    const barHeight = 20
    const x = (canvas.width - barWidth) / 2
    const y = canvas.height / 2
    
    ctx.fillStyle = '#333'
    ctx.fillRect(x, y, barWidth, barHeight)
    
    ctx.fillStyle = '#4ade80'
    ctx.fillRect(x, y, barWidth * this.progress, barHeight)
    
    // Texto
    ctx.fillStyle = '#fff'
    ctx.font = '16px sans-serif'
    ctx.textAlign = 'center'
    ctx.fillText(this.message, canvas.width / 2, y - 20)
    ctx.fillText(`${Math.round(this.progress * 100)}%`, canvas.width / 2, y + 50)
  }
}
</code></pre>
<h2>10) Boas práticas</h2>
<ul>
<li><strong>Mostre algo imediatamente</strong> — até mesmo uma imagem estática</li>
<li><strong>Carregue primeiro o que está visível</strong> — texturas da visualização atual</li>
<li><strong>Use placeholders</strong> — imagens de baixa resolução que são substituídas por versões melhores</li>
<li><strong>Pré-carregue a próxima fase</strong> — enquanto o jogador ainda está jogando</li>
<li><strong>Trate as falhas de forma adequada</strong> — lógica de novas tentativas e alternativas</li>
</ul>
<pre><code class="language-js">async function loadWithRetry(url, maxRetries = 3) {
  for (let i = 0; i &lt; maxRetries; i++) {
    try {
      return await fetch(url)
    } catch (err) {
      if (i === maxRetries - 1) throw err
      await new Promise(r =&gt; setTimeout(r, 1000 * (i + 1)))
    }
  }
}
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rapidamente</a></li>
<li><a href="/pt-BR/tutorials/service-workers-caching">Service workers para cache de jogos</a></li>
<li><a href="/pt-BR/tutorials/indexeddb-game-saves">Saves de jogos com IndexedDB</a></li>
<li><a href="/pt-BR/tutorials/web-workers-game-logic">Web Workers para a lógica do jogo</a> — decodifique e processe assets fora da thread principal</li>
<li><a href="/pt-BR/guides/game-assets-guide">Onde encontrar assets gratuitos para jogos</a> — fontes de modelos 3D, texturas e áudio para streaming</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">MDN: API Fetch</a> — respostas por streaming e ReadableStream</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream">MDN: ReadableStream</a> — processamento de blocos de dados transmitidos por streaming</li>
<li><a href="https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html">Especificação glTF 2.0</a> — o formato padrão para streaming de assets 3D</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Renderização de texto em jogos para web]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/text-rendering-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/text-rendering-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Renderize texto em jogos: texto no Canvas, fontes bitmap, fontes SDF e desempenho. De rótulos simples a sistemas de diálogo.]]></description>
            <content:encoded><![CDATA[<h1>Renderização de texto em jogos para web</h1>
<p>O texto está em toda parte nos jogos — pontuações, diálogos, menus e números de dano. Este tutorial aborda diferentes técnicas e quando usar cada uma.</p>
<h2>1) Noções básicas de texto no Canvas</h2>
<p>A abordagem mais simples:</p>
<pre><code class="language-js">ctx.font = '24px Arial'
ctx.fillStyle = '#ffffff'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('Pontuação: 1000', canvas.width / 2, 50)
</code></pre>
<p>Se você precisar de um espaçamento mais compacto ou mais amplo em títulos e rótulos da HUD, o contexto do Canvas agora oferece suporte direto a <code>letterSpacing</code> e <code>wordSpacing</code>. Ambos aceitam strings de comprimento CSS e alcançaram o status Baseline entre navegadores em 2025, então você não precisa mais desenhar os glifos um por um para simular o kerning:</p>
<pre><code class="language-js">ctx.font = '24px sans-serif'
ctx.letterSpacing = '2px'
ctx.wordSpacing = '6px'
ctx.fillText('FIM DE JOGO', 100, 100)
</code></pre>
<p>Redefina ambos como <code>'0px'</code> quando terminar, pois eles permanecem no contexto, assim como <code>font</code> e <code>fillStyle</code>.</p>
<h2>2) Texto com contorno</h2>
<pre><code class="language-js">function drawTextWithOutline(ctx, text, x, y, fillColor, outlineColor, outlineWidth = 2) {
  ctx.font = '24px Arial'
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'
  
  ctx.strokeStyle = outlineColor
  ctx.lineWidth = outlineWidth
  ctx.strokeText(text, x, y)
  
  ctx.fillStyle = fillColor
  ctx.fillText(text, x, y)
}

drawTextWithOutline(ctx, 'FIM DE JOGO', 400, 300, '#fff', '#000', 3)
</code></pre>
<h2>3) Texto com sombra</h2>
<pre><code class="language-js">function drawTextWithShadow(ctx, text, x, y, color, shadowOffset = 2) {
  ctx.font = '32px sans-serif'
  ctx.textAlign = 'left'
  ctx.textBaseline = 'top'
  
  // Shadow
  ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
  ctx.fillText(text, x + shadowOffset, y + shadowOffset)
  
  // Main text
  ctx.fillStyle = color
  ctx.fillText(text, x, y)
}
</code></pre>
<h2>4) Renderização com fonte bitmap</h2>
<p>Para jogos em pixel art ou para manter uma aparência consistente entre plataformas:</p>
<pre><code class="language-js">class BitmapFont {
  constructor(image, charWidth, charHeight, chars) {
    this.image = image
    this.charWidth = charWidth
    this.charHeight = charHeight
    this.chars = chars
  }
  
  draw(ctx, text, x, y, scale = 1) {
    const w = this.charWidth * scale
    const h = this.charHeight * scale
    
    for (let i = 0; i &lt; text.length; i++) {
      const charIndex = this.chars.indexOf(text[i].toUpperCase())
      if (charIndex === -1) continue
      
      const sx = (charIndex % 16) * this.charWidth
      const sy = Math.floor(charIndex / 16) * this.charHeight
      
      ctx.drawImage(
        this.image,
        sx, sy, this.charWidth, this.charHeight,
        Math.round(x + i * w), Math.round(y), w, h
      )
    }
  }
  
  measureText(text, scale = 1) {
    return text.length * this.charWidth * scale
  }
}

// Usage
const font = new BitmapFont(fontImage, 8, 8, 
  'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+:; '
)
font.draw(ctx, 'PONTOS: 1000', 10, 10, 2)
</code></pre>
<h2>5) Texto pré-renderizado para melhorar o desempenho</h2>
<p>Para textos estáticos, renderize uma vez e reutilize:</p>
<pre><code class="language-js">function createTextImage(text, font, color) {
  const tempCanvas = document.createElement('canvas')
  const tempCtx = tempCanvas.getContext('2d')
  
  tempCtx.font = font
  const metrics = tempCtx.measureText(text)
  
  tempCanvas.width = Math.ceil(metrics.width)
  tempCanvas.height = parseInt(font) * 1.5
  
  tempCtx.font = font
  tempCtx.fillStyle = color
  tempCtx.textBaseline = 'top'
  tempCtx.fillText(text, 0, 0)
  
  return tempCanvas
}

// Create once
const titleImage = createTextImage('MEU JOGO', '48px Impact', '#fff')

// Draw many times (fast)
ctx.drawImage(titleImage, x, y)
</code></pre>
<h2>6) Efeito de máquina de escrever</h2>
<pre><code class="language-js">class TypewriterText {
  constructor(text, x, y, charsPerSecond = 20) {
    this.fullText = text
    this.x = x
    this.y = y
    this.speed = 1 / charsPerSecond
    this.timer = 0
    this.charIndex = 0
  }
  
  update(dt) {
    this.timer += dt
    while (this.timer &gt;= this.speed &amp;&amp; this.charIndex &lt; this.fullText.length) {
      this.charIndex++
      this.timer -= this.speed
    }
  }
  
  draw(ctx) {
    ctx.font = '18px monospace'
    ctx.fillStyle = '#fff'
    ctx.fillText(this.fullText.slice(0, this.charIndex), this.x, this.y)
  }
  
  isComplete() {
    return this.charIndex &gt;= this.fullText.length
  }
  
  skip() {
    this.charIndex = this.fullText.length
  }
}
</code></pre>
<h2>7) Texto com quebra automática de linha</h2>
<pre><code class="language-js">function wrapText(ctx, text, maxWidth) {
  const words = text.split(' ')
  const lines = []
  let currentLine = ''
  
  for (const word of words) {
    const testLine = currentLine ? currentLine + ' ' + word : word
    const metrics = ctx.measureText(testLine)
    
    if (metrics.width &gt; maxWidth &amp;&amp; currentLine) {
      lines.push(currentLine)
      currentLine = word
    } else {
      currentLine = testLine
    }
  }
  
  if (currentLine) lines.push(currentLine)
  return lines
}

function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight) {
  const lines = wrapText(ctx, text, maxWidth)
  for (let i = 0; i &lt; lines.length; i++) {
    ctx.fillText(lines[i], x, y + i * lineHeight)
  }
}
</code></pre>
<h2>8) Sistema de caixas de diálogo</h2>
<pre><code class="language-js">class DialogueBox {
  constructor(x, y, width, height) {
    this.x = x
    this.y = y
    this.width = width
    this.height = height
    this.padding = 16
    this.lineHeight = 24
    this.text = ''
    this.displayedChars = 0
    this.charsPerSecond = 30
    this.timer = 0
  }
  
  show(text) {
    this.text = text
    this.displayedChars = 0
    this.timer = 0
  }
  
  update(dt) {
    if (this.displayedChars &lt; this.text.length) {
      this.timer += dt
      const charsToAdd = Math.floor(this.timer * this.charsPerSecond)
      this.displayedChars = Math.min(this.displayedChars + charsToAdd, this.text.length)
      this.timer = this.timer % (1 / this.charsPerSecond)
    }
  }
  
  skip() {
    this.displayedChars = this.text.length
  }
  
  isComplete() {
    return this.displayedChars &gt;= this.text.length
  }
  
  draw(ctx) {
    // Box background
    ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'
    ctx.fillRect(this.x, this.y, this.width, this.height)
    
    // Border
    ctx.strokeStyle = '#fff'
    ctx.lineWidth = 2
    ctx.strokeRect(this.x, this.y, this.width, this.height)
    
    // Text
    ctx.font = '18px sans-serif'
    ctx.fillStyle = '#fff'
    ctx.textBaseline = 'top'
    
    const displayText = this.text.slice(0, this.displayedChars)
    const maxWidth = this.width - this.padding * 2
    const lines = wrapText(ctx, displayText, maxWidth)
    
    for (let i = 0; i &lt; lines.length; i++) {
      ctx.fillText(
        lines[i],
        this.x + this.padding,
        this.y + this.padding + i * this.lineHeight
      )
    }
    
    // Continue indicator
    if (this.isComplete()) {
      ctx.fillText('▼', this.x + this.width - 24, this.y + this.height - 24)
    }
  }
}
</code></pre>
<h2>9) Números de dano flutuantes</h2>
<pre><code class="language-js">class FloatingText {
  constructor(text, x, y, color = '#fff') {
    this.text = text
    this.x = x
    this.y = y
    this.color = color
    this.vy = -60
    this.lifetime = 1
    this.age = 0
  }
  
  update(dt) {
    this.y += this.vy * dt
    this.vy *= 0.95
    this.age += dt
  }
  
  draw(ctx) {
    const alpha = 1 - (this.age / this.lifetime)
    ctx.font = 'bold 20px sans-serif'
    ctx.textAlign = 'center'
    ctx.fillStyle = this.color
    ctx.globalAlpha = alpha
    ctx.fillText(this.text, Math.round(this.x), Math.round(this.y))
    ctx.globalAlpha = 1
  }
  
  isExpired() {
    return this.age &gt;= this.lifetime
  }
}

// Manager
const floatingTexts = []

function spawnDamageNumber(damage, x, y) {
  const color = damage &gt; 50 ? '#ff4444' : '#ffff44'
  floatingTexts.push(new FloatingText(damage.toString(), x, y, color))
}

function updateFloatingTexts(dt) {
  for (let i = floatingTexts.length - 1; i &gt;= 0; i--) {
    floatingTexts[i].update(dt)
    if (floatingTexts[i].isExpired()) {
      floatingTexts.splice(i, 1)
    }
  }
}
</code></pre>
<h2>10) Texto em WebGL (fontes SDF)</h2>
<p>Para jogos em WebGL, fontes Signed Distance Field podem ser redimensionadas com nitidez:</p>
<pre><code class="language-js">// SDF shader fragment
const sdfFragmentShader = `
  precision mediump float;
  
  uniform sampler2D u_texture;
  uniform vec4 u_color;
  varying vec2 v_texCoord;
  
  void main() {
    float distance = texture2D(u_texture, v_texCoord).a;
    float alpha = smoothstep(0.4, 0.6, distance);
    gl_FragColor = vec4(u_color.rgb, u_color.a * alpha);
  }
`

// Libraries like msdf-bmfont-xml can generate SDF font atlases
</code></pre>
<p>Especificamente para cenas do three.js, <code>troika-three-text</code> é uma boa opção. Em vez de gerar um atlas previamente, ele analisa arquivos <code>.ttf</code>/<code>.otf</code>/<code>.woff</code> e gera os glifos SDF em tempo real em um web worker, além de cuidar do kerning, das ligaturas e dos sistemas de escrita da direita para a esquerda.</p>
<p>Para a maioria dos jogos 2D feitos com Canvas, fontes bitmap ou texto pré-renderizado no Canvas funcionam bem.</p>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/canvas-2d-game-loop">Loop de jogo com Canvas 2D</a></li>
<li><a href="/pt-BR/tutorials/pixel-art-rendering">Renderização de pixel art</a></li>
<li><a href="/pt-BR/tutorials/webgl-fundamentals-for-games">Fundamentos do WebGL</a></li>
<li><a href="/pt-BR/tutorials/responsive-game-canvas">Canvas responsivo para jogos</a> — dimensionamento de texto de acordo com a resolução e o DPI do Canvas</li>
<li><a href="/pt-BR/guides/game-assets-guide">Onde encontrar recursos gratuitos para jogos</a> — fontes de recursos de tipografia e interface de usuário</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tone.js para áudio de jogos]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/tone-js-game-audio</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/tone-js-game-audio</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Crie sons de jogo satisfatórios com Tone.js: efeitos sonoros baseados em sintetizadores, música dinâmica e áudio reativo que responde à jogabilidade.]]></description>
            <content:encoded><![CDATA[<h1>Tone.js para Áudio de Jogos</h1>
<p>O Tone.js funciona sobre a API Web Audio e torna prático o uso de áudio sintetizado. Em vez de carregar arquivos de áudio, você gera sons por meio de programação. Isso significa tempo de carregamento zero, variações infinitas e sons capazes de reagir à jogabilidade em tempo real.</p>
<p><strong>Experimente (clique nos botões para ouvir sons de jogo sintetizados):</strong></p>
<ToneDemo />
<h2>Por que Usar Tone.js em Vez de Arquivos de Áudio</h2>
<p>Arquivos de áudio funcionam bem para músicas e sons complexos. Mas, para efeitos sonoros de jogos, a síntese oferece vantagens reais. Você pode variar o tom e o tempo a cada reprodução para evitar que os sons pareçam repetitivos. Pode gerar sons dinamicamente com base no estado do jogo. E elimina completamente as requisições de rede.</p>
<p>A desvantagem é que o áudio sintetizado consome mais CPU do que a reprodução de um arquivo. Para jogos simples, isso não é um problema. Em jogos complexos, você pode combinar as duas abordagens.</p>
<h2>Configuração</h2>
<p>Carregue o Tone.js por uma CDN ou pelo npm:</p>
<pre><code class="language-html">&lt;script src=&quot;https://unpkg.com/tone@15/build/Tone.js&quot;&gt;&lt;/script&gt;
</code></pre>
<p>Ou com npm:</p>
<pre><code class="language-bash">npm install tone
</code></pre>
<pre><code class="language-js">import * as Tone from 'tone'
</code></pre>
<h2>O Problema da Reprodução Automática</h2>
<p>Os navegadores bloqueiam o áudio até que haja uma interação do usuário. O Tone.js lida com isso, mas você precisa chamar <code>Tone.start()</code> em resposta a um gesto do usuário:</p>
<pre><code class="language-js">document.addEventListener('click', async () =&gt; {
  await Tone.start()
  console.log('Audio ready')
}, { once: true })
</code></pre>
<p>Sem isso, seu AudioContext permanece suspenso e nada é reproduzido.</p>
<h2>Seu Primeiro Sintetizador</h2>
<p>Um sintetizador gera som. O mais simples é o <code>Tone.Synth</code>:</p>
<pre><code class="language-js">const synth = new Tone.Synth().toDestination()

// Play a note
synth.triggerAttackRelease('C4', '8n')
</code></pre>
<p><code>triggerAttackRelease</code> toca uma nota por determinada duração. <code>'C4'</code> é o dó central. <code>'8n'</code> é uma colcheia no andamento atual.</p>
<p>O sintetizador tem um oscilador (a fonte do som) e um envelope (como o som aumenta e diminui de intensidade). Você pode personalizar ambos:</p>
<pre><code class="language-js">const synth = new Tone.Synth({
  oscillator: { type: 'triangle' },
  envelope: {
    attack: 0.02,
    decay: 0.2,
    sustain: 0.3,
    release: 0.4,
  },
}).toDestination()
</code></pre>
<p>Os tipos de oscilador incluem <code>sine</code> (puro e suave), <code>triangle</code> (suave), <code>sawtooth</code> (vibrante) e <code>square</code> (oco e retrô).</p>
<h2>Criando um Sistema de Efeitos Sonoros</h2>
<p>Em um jogo, é útil ter vários sintetizadores para diferentes tipos de som. Veja um padrão que funciona bem:</p>
<pre><code class="language-js">let isInitialized = false
let softSynth = null
let subSynth = null
let noiseSynth = null
let sfxGain = null

function initSFX() {
  if (isInitialized) return
  
  // Master volume control
  sfxGain = new Tone.Gain(0.7).toDestination()
  
  // Soft synth for melodic sounds
  softSynth = new Tone.PolySynth(Tone.Synth, {
    maxPolyphony: 6,
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.2,
      sustain: 0.3,
      release: 0.4,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // Sub bass for deep impacts
  subSynth = new Tone.Synth({
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.3,
      sustain: 0.1,
      release: 0.3,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // Noise for texture
  noiseSynth = new Tone.NoiseSynth({
    noise: { type: 'pink' },
    envelope: {
      attack: 0.02,
      decay: 0.15,
      sustain: 0,
      release: 0.15,
    },
    volume: -20,
  }).connect(sfxGain)
  
  isInitialized = true
}
</code></pre>
<p><code>PolySynth</code> permite tocar várias notas ao mesmo tempo. O <code>Synth</code> comum é monofônico.</p>
<h2>Exemplos de Sons de Jogos</h2>
<p>Veja alguns sons comuns em jogos e como criá-los:</p>
<p><strong>Coleta de moeda</strong> — notas ascendentes transmitem uma sensação de recompensa:</p>
<pre><code class="language-js">function coinCollect() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('C4', '8n', now, 0.3)
  softSynth.triggerAttackRelease('E4', '8n', now + 0.1, 0.35)
  softSynth.triggerAttackRelease('G4', '4n', now + 0.2, 0.4)
}
</code></pre>
<p><strong>Pulo</strong> — varredura rápida para cima:</p>
<pre><code class="language-js">function jump() {
  const synth = new Tone.Synth({
    oscillator: { type: 'square' },
    envelope: { attack: 0.01, decay: 0.1, sustain: 0, release: 0.1 },
  }).toDestination()
  
  const now = Tone.now()
  synth.frequency.setValueAtTime(150, now)
  synth.frequency.exponentialRampToValueAtTime(400, now + 0.1)
  synth.triggerAttackRelease('C4', '8n', now, 0.2)
}
</code></pre>
<p><strong>Golpe/dano</strong> — som descendente com ruído:</p>
<pre><code class="language-js">function hit() {
  const now = Tone.now()
  subSynth.triggerAttackRelease('E2', '4n', now, 0.5)
  noiseSynth.triggerAttackRelease('4n', now, 0.25)
}
</code></pre>
<p><strong>Morte</strong> — notas descendentes e tristes:</p>
<pre><code class="language-js">function death() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('E4', '4n', now, 0.4)
  softSynth.triggerAttackRelease('C4', '4n', now + 0.2, 0.35)
  softSynth.triggerAttackRelease('A3', '4n', now + 0.4, 0.3)
}
</code></pre>
<h2>Adicionando Efeitos</h2>
<p>Os efeitos deixam os sons mais ricos. Conecte-os em cadeia entre o sintetizador e o destino:</p>
<pre><code class="language-js">const filter = new Tone.Filter({
  frequency: 3000,
  type: 'lowpass',
  rolloff: -12,
})

const reverb = new Tone.Reverb({
  decay: 1.2,
  wet: 0.25,
})

const gain = new Tone.Gain(0.7)

// Chain: synth -&gt; filter -&gt; reverb -&gt; gain -&gt; destination
filter.connect(reverb)
reverb.connect(gain)
gain.toDestination()

const synth = new Tone.Synth().connect(filter)
</code></pre>
<p>O filtro passa-baixas reduz frequências agudas desagradáveis. O reverb acrescenta espaço. O ganho controla o volume.</p>
<h2>Limitação de Frequência</h2>
<p>Se o jogo disparar muitos sons rapidamente, eles poderão se sobrepor e virar uma confusão. Adicione uma limitação de frequência:</p>
<pre><code class="language-js">const MIN_INTERVAL_MS = 35
let lastNoteTime = 0

function canPlayNote() {
  const now = performance.now()
  if (now - lastNoteTime &lt; MIN_INTERVAL_MS) {
    return false
  }
  lastNoteTime = now
  return true
}

function playSound() {
  if (!canPlayNote()) return
  synth.triggerAttackRelease('C4', '8n')
}
</code></pre>
<h2>Música de Fundo Dinâmica</h2>
<p>Para criar música que reage à jogabilidade, use um sistema baseado em batidas:</p>
<pre><code class="language-js">const BPM = 140
const BEAT_MS = (60 / BPM) * 1000

let bassSynth = null
let kickDrum = null
let beatInterval = null
let beatCount = 0
let energy = 0.3

const PROGRESSION = [
  { root: 'E2', chord: ['E3', 'G3', 'B3'] },
  { root: 'C2', chord: ['C3', 'E3', 'G3'] },
  { root: 'D2', chord: ['D3', 'F#3', 'A3'] },
  { root: 'A1', chord: ['A2', 'C3', 'E3'] },
]

function playBeat() {
  const beatInMeasure = beatCount % 8
  const chordIndex = Math.floor(beatCount / 8) % PROGRESSION.length
  const chord = PROGRESSION[chordIndex]
  const now = Tone.now()
  
  // Bass on beat 1
  if (beatInMeasure === 0) {
    bassSynth.triggerAttackRelease(chord.root, '4n', now, 0.6)
  }
  
  // Kick drum - only when energy is high
  if (beatInMeasure === 0 &amp;&amp; energy &gt; 0.3) {
    kickDrum.triggerAttackRelease('C1', '8n', now, 0.4)
  }
  
  beatCount++
}

function startMusic() {
  beatInterval = setInterval(playBeat, BEAT_MS)
}

function stopMusic() {
  clearInterval(beatInterval)
}

// Game events affect energy
function onEnemyKill() {
  energy = Math.min(1, energy + 0.1)
}
</code></pre>
<p>A intensidade da música muda de acordo com o que está acontecendo no jogo.</p>
<h2>Bateria com MembraneSynth e NoiseSynth</h2>
<p>Para percussão, use sintetizadores especializados:</p>
<pre><code class="language-js">// Kick drum
const kick = new Tone.MembraneSynth({
  pitchDecay: 0.05,
  octaves: 4,
  envelope: {
    attack: 0.001,
    decay: 0.2,
    sustain: 0,
    release: 0.1,
  },
  volume: -14,
}).toDestination()

kick.triggerAttackRelease('C1', '8n')

// Snare
const snare = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.002,
    decay: 0.15,
    sustain: 0,
    release: 0.1,
  },
  volume: -12,
}).toDestination()

snare.triggerAttackRelease('8n')

// Hi-hat
const hihat = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.001,
    decay: 0.04,
    sustain: 0,
    release: 0.03,
  },
  volume: -22,
}).toDestination()

hihat.triggerAttackRelease('32n')
</code></pre>
<h2>Limpeza</h2>
<p>Descarte os sintetizadores quando terminar para liberar recursos:</p>
<pre><code class="language-js">function cleanup() {
  if (softSynth) softSynth.dispose()
  if (subSynth) subSynth.dispose()
  if (noiseSynth) noiseSynth.dispose()
  if (sfxGain) sfxGain.dispose()
}
</code></pre>
<h2>Erros Comuns</h2>
<p><strong>Não aguardar <code>Tone.start()</code></strong> — o áudio não será reproduzido até que o contexto seja iniciado durante uma interação do usuário.</p>
<p><strong>Criar sintetizadores para cada som</strong> — crie os sintetizadores uma vez durante a inicialização e reutilize-os. Criar novos a cada vez causa vazamentos de memória.</p>
<p><strong>Sons demais sobrepostos</strong> — use limitação de frequência e desduplicação para evitar que o áudio vire uma massa indistinta.</p>
<p><strong>Frequências desagradáveis</strong> — use filtros passa-baixas e tipos de oscilador mais suaves (<code>sine</code>, <code>triangle</code>) para criar sons que não incomodem os jogadores.</p>
<h2>Mais Recursos</h2>
<p><a href="/pt-BR/tutorials/web-audio-api-games">API Web Audio para jogos</a> aborda a API subjacente e o carregamento de arquivos de áudio.</p>
<p>A <a href="https://tonejs.github.io/">documentação do Tone.js</a> contém a referência completa da API.</p>
<p><a href="/pt-BR/guides/game-assets-guide">Onde Encontrar Recursos Gratuitos para Jogos</a> inclui provedores de áudio e efeitos sonoros, como o Freesound.</p>
<p><a href="/pt-BR/tutorials/mobile-friendly-web-games">Jogos web adaptados para dispositivos móveis</a> aborda as restrições de áudio do iOS e padrões de toque para desbloquear o áudio.</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Web Audio API para jogos]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/web-audio-api-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/web-audio-api-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Implemente áudio para jogos com a Web Audio API: efeitos sonoros, música, áudio espacial e mixagem. Padrões práticos que funcionam.]]></description>
            <content:encoded><![CDATA[<h1>Web Audio API para jogos</h1>
<p>Um bom áudio transforma um jogo. Um jogo sem som parece sem vida. A Web Audio API oferece efeitos sonoros de baixa latência, reprodução de música e até áudio espacial 3D, tudo no navegador.</p>
<p><strong>Experimente (clique em um botão para ouvir sons sintetizados):</strong></p>
<AudioDemo />
<h2>O problema da reprodução automática</h2>
<p>Antes de qualquer coisa, você precisa entender as restrições de reprodução automática. Os navegadores bloqueiam o áudio até que o usuário interaja com a página. Isso é algo positivo (ninguém quer sites disparando sons ao carregar), mas significa que você precisa inicializar o áudio em resposta a um clique, toque ou pressionamento de tecla.</p>
<pre><code class="language-js">let audioCtx = null

function initAudio() {
  if (!audioCtx) {
    audioCtx = new AudioContext()
  }
  if (audioCtx.state === 'suspended') {
    audioCtx.resume()
  }
  return audioCtx
}

// Inicializa na primeira interação
document.addEventListener('click', () =&gt; initAudio(), { once: true })
document.addEventListener('keydown', () =&gt; initAudio(), { once: true })
</code></pre>
<p>Se você tentar reproduzir áudio antes disso, a operação falhará silenciosamente ou o AudioContext permanecerá suspenso. Isso pega muitos desenvolvedores de surpresa.</p>
<h2>Carregando sons</h2>
<p>Os arquivos de áudio precisam ser decodificados antes da reprodução. Busque o arquivo, decodifique-o e mantenha o buffer disponível para reutilização:</p>
<pre><code class="language-js">async function loadSound(url) {
  const response = await fetch(url)
  const arrayBuffer = await response.arrayBuffer()
  const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)
  return audioBuffer
}
</code></pre>
<p>A decodificação leva tempo, então carregue todos os sons durante uma tela de carregamento, não quando precisar reproduzi-los.</p>
<h2>Reproduzindo sons</h2>
<p>Para reproduzir um som, crie uma fonte de buffer, conecte-a por meio de um nó de ganho para controlar o volume e inicie-a:</p>
<pre><code class="language-js">function playSound(buffer, volume = 1) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  
  const gainNode = audioCtx.createGain()
  gainNode.gain.value = volume
  
  source.connect(gainNode)
  gainNode.connect(audioCtx.destination)
  
  source.start(0)
  return source
}
</code></pre>
<p>As fontes de buffer só podem ser usadas uma vez. Depois que uma fonte termina de tocar, você não pode reiniciá-la. Crie uma nova a cada reprodução. Isso pode parecer um desperdício, mas é assim que a API funciona, e os navegadores são otimizados para isso.</p>
<h2>Um gerenciador de sons</h2>
<p>Para um jogo de verdade, reúna tudo em uma classe gerenciadora:</p>
<pre><code class="language-js">class SoundManager {
  constructor() {
    this.ctx = null
    this.sounds = new Map()
    this.masterGain = null
  }
  
  init() {
    this.ctx = new AudioContext()
    this.masterGain = this.ctx.createGain()
    this.masterGain.connect(this.ctx.destination)
  }
  
  async load(name, url) {
    const response = await fetch(url)
    const buffer = await this.ctx.decodeAudioData(await response.arrayBuffer())
    this.sounds.set(name, buffer)
  }
  
  play(name, volume = 1) {
    const buffer = this.sounds.get(name)
    if (!buffer) return
    
    const source = this.ctx.createBufferSource()
    source.buffer = buffer
    
    const gain = this.ctx.createGain()
    gain.gain.value = volume
    
    source.connect(gain)
    gain.connect(this.masterGain)
    source.start(0)
    
    return source
  }
  
  setMasterVolume(v) {
    this.masterGain.gain.value = v
  }
}
</code></pre>
<p>O nó de ganho mestre permite implementar um controle deslizante de volume global. Todos os sons passam por ele.</p>
<h2>Música de fundo</h2>
<p>A música precisa ser reproduzida em loop e oferecer a possibilidade de ser interrompida ou desaparecer gradualmente:</p>
<pre><code class="language-js">class MusicPlayer {
  constructor(ctx, masterGain) {
    this.ctx = ctx
    this.masterGain = masterGain
    this.currentTrack = null
    this.gain = ctx.createGain()
    this.gain.connect(masterGain)
  }
  
  play(buffer, volume = 0.5) {
    this.stop()
    
    this.currentTrack = this.ctx.createBufferSource()
    this.currentTrack.buffer = buffer
    this.currentTrack.loop = true
    this.currentTrack.connect(this.gain)
    this.gain.gain.value = volume
    this.currentTrack.start(0)
  }
  
  stop() {
    if (this.currentTrack) {
      this.currentTrack.stop()
      this.currentTrack = null
    }
  }
  
  fadeOut(duration = 1) {
    this.gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + duration)
  }
}
</code></pre>
<p>O método <code>linearRampToValueAtTime</code> cria transições suaves em vez de cortes abruptos.</p>
<h2>Áudio espacial</h2>
<p>Para jogos 3D, você pode posicionar os sons no espaço. Eles ficam mais baixos à medida que se afastam do ouvinte:</p>
<pre><code class="language-js">function playSpatialSound(buffer, x, y, z) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  
  const panner = audioCtx.createPanner()
  panner.panningModel = 'HRTF'
  panner.distanceModel = 'inverse'
  panner.refDistance = 1
  panner.maxDistance = 100
  panner.positionX.value = x
  panner.positionY.value = y
  panner.positionZ.value = z
  
  source.connect(panner)
  panner.connect(audioCtx.destination)
  source.start(0)
}

function updateListener(x, y, z, fx, fy, fz) {
  const listener = audioCtx.listener
  listener.positionX.value = x
  listener.positionY.value = y
  listener.positionZ.value = z
  listener.forwardX.value = fx
  listener.forwardY.value = fy
  listener.forwardZ.value = fz
  listener.upX.value = 0
  listener.upY.value = 1
  listener.upZ.value = 0
}
</code></pre>
<p>Chame <code>updateListener</code> a cada quadro usando a posição da câmera ou do jogador. O panner ajusta automaticamente o volume e a panoramização estéreo com base na distância e na direção.</p>
<h2>Sons de disparos rápidos</h2>
<p>Para sons reproduzidos muitas vezes por segundo (tiros, passos), você não quer que instâncias sobrepostas se acumulem. Um padrão simples de pool ajuda:</p>
<pre><code class="language-js">class SoundPool {
  constructor(ctx, buffer, size = 8) {
    this.sources = []
    this.index = 0
    this.ctx = ctx
    this.buffer = buffer
    this.size = size
  }
  
  play(volume = 1) {
    const source = this.ctx.createBufferSource()
    source.buffer = this.buffer
    
    const gain = this.ctx.createGain()
    gain.gain.value = volume
    
    source.connect(gain)
    gain.connect(this.ctx.destination)
    source.start(0)
    
    this.index = (this.index + 1) % this.size
  }
}
</code></pre>
<p>O pool limita quantas instâncias do mesmo som podem ser reproduzidas simultaneamente. Quando você atinge o limite, a mais antiga é substituída.</p>
<h2>Formatos de áudio</h2>
<p>Formatos diferentes funcionam melhor em situações diferentes:</p>
<p>MP3 funciona em todos os lugares e é uma boa opção para música. OGG Vorbis oferece melhor qualidade com o mesmo tamanho de arquivo. O Safari resistiu por muito tempo, mas adicionou reprodução nativa de Ogg Vorbis no Safari 18.4, então todos os principais navegadores atuais agora oferecem suporte. Ainda é recomendável disponibilizar MP3 como alternativa se você precisa oferecer suporte a versões mais antigas do Safari. AAC/M4A funciona bem em dispositivos Apple. WAV não tem compressão e gera arquivos grandes, mas é decodificado instantaneamente, o que é bom para efeitos sonoros curtos quando o tamanho do arquivo não é um problema. WebM/Opus oferece a melhor relação entre qualidade e tamanho, mas só funciona em navegadores modernos.</p>
<p>Para garantir compatibilidade máxima, ofereça alternativas:</p>
<pre><code class="language-js">const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'
</code></pre>
<h2>Truques úteis</h2>
<p><strong>Variação de tom</strong> faz com que sons repetidos pareçam menos robóticos:</p>
<pre><code class="language-js">function playWithPitchVariation(buffer, variance = 0.1) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  source.playbackRate.value = 1 + (Math.random() - 0.5) * variance
  source.connect(audioCtx.destination)
  source.start(0)
}
</code></pre>
<p>Cada tiro ou passo soa ligeiramente diferente.</p>
<p><strong>Ducking</strong> reduz o volume da música durante diálogos ou sons importantes:</p>
<pre><code class="language-js">function duckMusic(musicGain, duration = 0.3) {
  musicGain.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + duration)
}

function unduckMusic(musicGain, duration = 0.3) {
  musicGain.gain.linearRampToValueAtTime(1, audioCtx.currentTime + duration)
}
</code></pre>
<h2>Armadilhas no iOS</h2>
<p>O iOS tem restrições de áudio mais rígidas do que outras plataformas. O AudioContext precisa ser criado e retomado durante um gesto do usuário. Algumas versões do iOS também exigem que você reproduza um som (mesmo que seja silencioso) durante o gesto, e não apenas crie o contexto. Se o áudio funciona no desktop, mas não no iOS, esse provavelmente é o motivo.</p>
<h2>Mais recursos</h2>
<p><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rápido</a> aborda a compactação de recursos, incluindo áudio.</p>
<p><a href="/pt-BR/tutorials/mobile-friendly-web-games">Jogos web compatíveis com dispositivos móveis</a> apresenta mais informações sobre problemas específicos do iOS.</p>
<p><a href="/pt-BR/tutorials/pwa-offline-games">PWA para jogos offline</a> mostra como armazenar arquivos de áudio em cache para jogar offline.</p>
<p><a href="/pt-BR/tutorials/tone-js-game-audio">Tone.js para áudio de jogos</a> aborda a geração procedural de sons e sistemas de música usando uma biblioteca de nível mais alto.</p>
<p><a href="/pt-BR/guides/game-assets-guide">Onde encontrar recursos gratuitos para jogos</a> apresenta fontes gratuitas de efeitos sonoros e músicas, como Freesound e Poly Haven.</p>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API">MDN: Web Audio API</a> — referência completa da API</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioContext">MDN: AudioContext</a> — criação e gerenciamento de contextos de áudio</li>
<li><a href="https://www.w3.org/TR/webaudio/">Especificação da Web Audio API</a> — especificação do W3C</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Web Workers para lógica de jogos]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/web-workers-game-logic</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/web-workers-game-logic</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Transfira cálculos pesados para Web Workers: física, busca de caminhos, IA e geração procedural sem bloquear a thread principal.]]></description>
            <content:encoded><![CDATA[<h1>Web Workers para lógica de jogos</h1>
<p>Os Web Workers permitem executar JavaScript em uma thread em segundo plano. Para jogos, isso significa que física, IA e geração procedural podem ser executadas sem causar quedas na taxa de quadros.</p>
<h2>1) Quando usar Workers</h2>
<p>Boas opções:</p>
<ul>
<li>Simulação de física</li>
<li>Busca de caminhos (A*, navmesh)</li>
<li>Tomada de decisões de IA</li>
<li>Geração procedural</li>
<li>Processamento de assets (manipulação de imagens, compactação)</li>
<li>Matemática complexa (FFT, detecção de colisões)</li>
</ul>
<p>Não são ideais para:</p>
<ul>
<li>Renderização (Workers não podem acessar diretamente o DOM/Canvas)</li>
<li>Tarefas muito pequenas (o custo adicional da thread não compensa)</li>
</ul>
<h2>2) Criando um Worker básico</h2>
<p><strong>worker.js:</strong></p>
<pre><code class="language-js">self.onmessage = (e) =&gt; {
  const { type, data } = e.data
  
  if (type === 'calculate') {
    const result = heavyCalculation(data)
    self.postMessage({ type: 'result', data: result })
  }
}

function heavyCalculation(input) {
  // Expensive work here
  return input * 2
}
</code></pre>
<p><strong>main.js:</strong></p>
<pre><code class="language-js">const worker = new Worker('worker.js')

worker.onmessage = (e) =&gt; {
  const { type, data } = e.data
  if (type === 'result') {
    console.log('Got result:', data)
  }
}

worker.postMessage({ type: 'calculate', data: 42 })
</code></pre>
<h2>3) Workers inline (sem arquivo separado)</h2>
<pre><code class="language-js">function createInlineWorker(fn) {
  const blob = new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })
  return new Worker(URL.createObjectURL(blob))
}

const worker = createInlineWorker(() =&gt; {
  self.onmessage = (e) =&gt; {
    const result = e.data * 2
    self.postMessage(result)
  }
})
</code></pre>
<h2>4) Workers como módulos ES</h2>
<p>Você pode escrever o código do Worker como um módulo ES e usar <code>import</code> dentro dele, em vez de <code>importScripts</code>. Passe <code>{ type: 'module' }</code> ao construtor:</p>
<pre><code class="language-js">const worker = new Worker('physics-worker.js', { type: 'module' })
</code></pre>
<p>Dentro do Worker, você pode usar <code>import</code> para carregar funções auxiliares compartilhadas de matemática, colisão ou IA da mesma forma que na thread principal, evitando duplicar a lógica do jogo. Workers de módulo têm suporte no Chrome e Edge 80+, Safari 15+ e Firefox 114+.</p>
<h2>5) Exemplo de Worker de física</h2>
<p><strong>physics-worker.js:</strong></p>
<pre><code class="language-js">const bodies = []
const FIXED_DT = 1 / 60

self.onmessage = (e) =&gt; {
  const { type, data } = e.data
  
  switch (type) {
    case 'init':
      initWorld(data)
      break
    case 'step':
      step()
      break
    case 'addBody':
      bodies.push(data)
      break
  }
}

function initWorld(config) {
  // Initialize physics world
}

function step() {
  // Update physics
  for (const body of bodies) {
    body.vy += 9.8 * FIXED_DT // Gravity
    body.x += body.vx * FIXED_DT
    body.y += body.vy * FIXED_DT
  }
  
  // Send positions back
  self.postMessage({
    type: 'positions',
    data: bodies.map(b =&gt; ({ id: b.id, x: b.x, y: b.y, rotation: b.rotation }))
  })
}
</code></pre>
<h2>6) Objetos transferíveis para melhorar o desempenho</h2>
<p>Grandes volumes de dados podem ser transferidos sem cópia:</p>
<pre><code class="language-js">// Main thread
const positions = new Float32Array(1000)
worker.postMessage(positions, [positions.buffer])
// positions is now unusable here (transferred)

// Worker
self.onmessage = (e) =&gt; {
  const positions = e.data
  // Work with positions
  self.postMessage(positions, [positions.buffer])
}
</code></pre>
<h2>7) Worker para busca de caminhos</h2>
<p><strong>pathfinding-worker.js:</strong></p>
<pre><code class="language-js">let grid = null

self.onmessage = (e) =&gt; {
  const { type, data } = e.data
  
  if (type === 'setGrid') {
    grid = data
  }
  
  if (type === 'findPath') {
    const path = aStar(data.start, data.end, grid)
    self.postMessage({ type: 'path', id: data.id, path })
  }
}

function aStar(start, end, grid) {
  // A* implementation
  const openSet = [start]
  const cameFrom = new Map()
  const gScore = new Map()
  gScore.set(key(start), 0)
  
  while (openSet.length &gt; 0) {
    // ... A* logic
  }
  
  return reconstructPath(cameFrom, end)
}

function key(pos) {
  return `${pos.x},${pos.y}`
}
</code></pre>
<h2>8) Pool de Workers para tarefas paralelas</h2>
<pre><code class="language-js">class WorkerPool {
  constructor(workerUrl, size = navigator.hardwareConcurrency || 4) {
    this.workers = []
    this.queue = []
    this.available = []
    
    for (let i = 0; i &lt; size; i++) {
      const worker = new Worker(workerUrl)
      worker.onmessage = (e) =&gt; this.handleResult(worker, e)
      this.workers.push(worker)
      this.available.push(worker)
    }
  }
  
  run(data) {
    return new Promise((resolve) =&gt; {
      const task = { data, resolve }
      
      if (this.available.length &gt; 0) {
        this.dispatch(this.available.pop(), task)
      } else {
        this.queue.push(task)
      }
    })
  }
  
  dispatch(worker, task) {
    worker._currentTask = task
    worker.postMessage(task.data)
  }
  
  handleResult(worker, e) {
    const task = worker._currentTask
    task.resolve(e.data)
    
    if (this.queue.length &gt; 0) {
      this.dispatch(worker, this.queue.shift())
    } else {
      this.available.push(worker)
    }
  }
  
  terminate() {
    this.workers.forEach(w =&gt; w.terminate())
  }
}
</code></pre>
<h2>9) SharedArrayBuffer para sincronização em tempo real</h2>
<p>Com o isolamento entre origens ativado, você pode compartilhar memória:</p>
<pre><code class="language-js">// Main thread
const shared = new SharedArrayBuffer(1024)
const positions = new Float32Array(shared)

worker.postMessage({ type: 'init', buffer: shared })

// Worker reads/writes directly to shared memory
// No postMessage overhead for position updates
</code></pre>
<h2>10) OffscreenCanvas para Workers</h2>
<p>Renderize em um Worker. O OffscreenCanvas agora está amplamente disponível como recurso Baseline no Chrome, Edge, Firefox e Safari (o Safari adicionou suporte na versão 17.0 para macOS e iOS):</p>
<pre><code class="language-js">// Main thread
const canvas = document.getElementById('game')
const offscreen = canvas.transferControlToOffscreen()
worker.postMessage({ canvas: offscreen }, [offscreen])

// Worker
self.onmessage = (e) =&gt; {
  const canvas = e.data.canvas
  const ctx = canvas.getContext('2d')
  
  function render() {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    // Draw...
    requestAnimationFrame(render)
  }
  render()
}
</code></pre>
<h2>11) Tratamento de erros</h2>
<pre><code class="language-js">worker.onerror = (e) =&gt; {
  console.error('Worker error:', e.message, e.filename, e.lineno)
}

// In worker
self.onerror = (e) =&gt; {
  self.postMessage({ type: 'error', message: e.message })
}
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rapidamente</a></li>
<li><a href="/pt-BR/tutorials/coop-coep-sharedarraybuffer">Ative threads do Wasm (SharedArrayBuffer)</a></li>
<li><a href="/pt-BR/tutorials/canvas-2d-game-loop">Loop de jogo com Canvas 2D</a></li>
<li><a href="/pt-BR/tutorials/game-physics-libraries">Bibliotecas de física para jogos</a> — executando Rapier ou Cannon-es em um Worker</li>
<li><a href="/pt-BR/tutorials/streaming-asset-loading">Carregamento de assets por streaming</a> — decodificando assets fora da thread principal</li>
<li><a href="/pt-BR/guides/web-games-stack-2026">Stack de tecnologia para jogos web em 2026</a> — como os Workers se encaixam na stack WebGL/WebGPU/Wasm</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">MDN: API Web Workers</a> — referência completa da API</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers">MDN: Como usar Web Workers</a> — guia passo a passo</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects">MDN: Objetos transferíveis</a> — transferência de dados sem cópia</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer">MDN: SharedArrayBuffer</a> — memória compartilhada entre threads</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Fundamentos de WebGL para desenvolvedores de jogos]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/webgl-fundamentals-for-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/webgl-fundamentals-for-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Aprenda os conceitos básicos de WebGL para jogos: shaders, buffers, texturas e o loop de renderização. Código prático que você pode copiar e usar como base.]]></description>
            <content:encoded><![CDATA[<h1>Fundamentos de WebGL para desenvolvedores de jogos</h1>
<p>O WebGL oferece acesso direto à GPU no navegador. Ele é a base dos jogos web 3D (e 2D de alto desempenho). Este tutorial aborda os conceitos essenciais.</p>
<h2>1) Obtendo um contexto WebGL</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl')

if (!gl) {
  // Fallback or error message
  console.error('WebGL not supported')
}
</code></pre>
<p>Prefira <code>webgl2</code> quando estiver disponível — ele é amplamente compatível e oferece recursos melhores.</p>
<h2>2) O pipeline de renderização (simplificado)</h2>
<ol>
<li>O <strong>shader de vértice</strong> é executado uma vez por vértice. Ele posiciona sua geometria.</li>
<li>O <strong>shader de fragmento</strong> é executado uma vez por pixel. Ele colore a saída.</li>
<li>Os buffers armazenam dados (posições, UVs, cores).</li>
<li>As texturas são imagens amostradas pelos shaders.</li>
</ol>
<h2>3) Um par mínimo de shaders</h2>
<p><strong>Shader de vértice:</strong></p>
<pre><code class="language-glsl">#version 300 es
in vec2 a_position;
void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);
}
</code></pre>
<p><strong>Shader de fragmento:</strong></p>
<pre><code class="language-glsl">#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
  fragColor = vec4(1.0, 0.5, 0.2, 1.0); // orange
}
</code></pre>
<h2>4) Compilando e vinculando shaders</h2>
<pre><code class="language-js">function createShader(gl, type, source) {
  const shader = gl.createShader(type)
  gl.shaderSource(shader, source)
  gl.compileShader(shader)
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error(gl.getShaderInfoLog(shader))
    gl.deleteShader(shader)
    return null
  }
  return shader
}

function createProgram(gl, vs, fs) {
  const program = gl.createProgram()
  gl.attachShader(program, vs)
  gl.attachShader(program, fs)
  gl.linkProgram(program)
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error(gl.getProgramInfoLog(program))
    return null
  }
  return program
}
</code></pre>
<h2>5) Criando buffers</h2>
<pre><code class="language-js">const positions = new Float32Array([
  -0.5, -0.5,
   0.5, -0.5,
   0.0,  0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW)
</code></pre>
<h2>6) Conectando buffers aos shaders</h2>
<pre><code class="language-js">const positionLoc = gl.getAttribLocation(program, 'a_position')
gl.enableVertexAttribArray(positionLoc)
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0)
</code></pre>
<h2>7) O loop do jogo</h2>
<pre><code class="language-js">function render() {
  gl.viewport(0, 0, canvas.width, canvas.height)
  gl.clearColor(0, 0, 0, 1)
  gl.clear(gl.COLOR_BUFFER_BIT)

  gl.useProgram(program)
  gl.drawArrays(gl.TRIANGLES, 0, 3)

  requestAnimationFrame(render)
}
render()
</code></pre>
<h2>8) Carregando texturas</h2>
<pre><code class="language-js">function loadTexture(gl, url) {
  const texture = gl.createTexture()
  gl.bindTexture(gl.TEXTURE_2D, texture)

  // Placeholder pixel until image loads
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
    new Uint8Array([255, 0, 255, 255]))

  const img = new Image()
  img.onload = () =&gt; {
    gl.bindTexture(gl.TEXTURE_2D, texture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
    gl.generateMipmap(gl.TEXTURE_2D)
  }
  img.src = url
  return texture
}
</code></pre>
<h2>9) Armadilhas comuns</h2>
<ul>
<li><strong>Texturas com dimensões em potências de dois</strong> não são estritamente obrigatórias no WebGL 2, mas os mipmaps funcionam melhor com elas.</li>
<li><strong>Perda de contexto</strong>: trate os eventos <code>webglcontextlost</code> — navegadores móveis descartam contextos quando há pressão sobre a memória.</li>
<li><strong>Texturas de ponto flutuante</strong> exigem a verificação de extensões (<code>EXT_color_buffer_float</code>).</li>
</ul>
<h2>10) Quando usar uma biblioteca</h2>
<p>O WebGL puro é verboso. Para a maioria dos jogos, considere:</p>
<ul>
<li><strong>Three.js</strong> — 3D completo</li>
<li><strong>PixiJS</strong> — renderização 2D rápida</li>
<li><strong>PlayCanvas</strong> — engine de jogos com editor</li>
</ul>
<p>Use WebGL puro quando precisar de:</p>
<ul>
<li>controle máximo</li>
<li>um renderizador personalizado</li>
<li>aprender como as coisas realmente funcionam</li>
</ul>
<p>Uma observação para 2026: o WebGPU agora vem ativado por padrão em todos os principais navegadores (Chrome e Edge desde a versão 113, Firefox 141 no Windows e 145 no macOS, e Safari no iOS 26, iPadOS 26 e macOS Tahoe 26). Os fundamentos apresentados aqui ainda se aplicam, pois bibliotecas como Three.js executam seu renderizador WebGPU com fallback automático para WebGL 2, então o WebGL 2 continua sendo uma base confiável. Se você está começando hoje um novo renderizador do zero, leia o tutorial de WebGPU junto com este antes de escolher uma API.</p>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/webgpu-getting-started">WebGPU para jogos</a></li>
<li><a href="/pt-BR/tutorials/canvas-2d-game-loop">Loop de jogo com Canvas 2D</a></li>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rápido</a></li>
<li><a href="/pt-BR/guides/web-games-stack-2026">Stack de tecnologias para jogos web em 2026</a> — quando escolher WebGL, WebGPU ou Wasm</li>
<li><a href="/pt-BR/tutorials/game-physics-libraries">Bibliotecas de física para jogos</a> — engines de física que renderizam por meio do WebGL</li>
<li><a href="/pt-BR/guides/web-game-engines-comparison">Comparativo de engines para jogos web</a> — Three.js, PixiJS, PlayCanvas e outras</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API">MDN: API WebGL</a> — referência completa da API</li>
<li><a href="https://webgl2fundamentals.org/">Fundamentos do WebGL2</a> — série aprofundada de tutoriais sobre WebGL2</li>
<li><a href="https://thebookofshaders.com/">O Livro dos Shaders</a> — guia interativo sobre shaders de fragmento GLSL</li>
<li><a href="https://threejs.org/docs/">Documentação do Three.js</a> — o framework WebGL mais popular</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Primeiros passos com WebGPU para desenvolvedores de jogos]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/webgpu-getting-started</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/webgpu-getting-started</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Comece a usar WebGPU: configuração do dispositivo, pipelines, shaders e renderização. Uma API moderna de GPU para a próxima geração de jogos web.]]></description>
            <content:encoded><![CDATA[<h1>Primeiros passos com WebGPU para desenvolvedores de jogos</h1>
<p>WebGPU é o sucessor do WebGL. É uma API de nível mais baixo, mais explícita e projetada para GPUs modernas. Se você está começando um novo projeto 3D em 2026, vale a pena aprender WebGPU.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0">
<iframe src="https://www.youtube.com/embed/KTFFdZSDiTU" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture" allowfullscreen></iframe>
</div>
<p style="font-size:0.9rem;color:var(--vp-c-text-2);margin-top:-0.5rem">Tutorial de WebGPU: 10 projetos que abrangem desde a configuração até a renderização 3D com iluminação e animação</p>
<h2>1) Verifique a compatibilidade do navegador</h2>
<pre><code class="language-js">if (!navigator.gpu) {
  console.error('WebGPU not supported')
}
</code></pre>
<p>Em 2026, o WebGPU está disponível no Chrome, Edge, Firefox e Safari.</p>
<h2>2) Obtenha um dispositivo</h2>
<pre><code class="language-js">const adapter = await navigator.gpu.requestAdapter()
const device = await adapter.requestDevice()
</code></pre>
<p>O adaptador representa a GPU física. O dispositivo é sua interface com ela.</p>
<p>Ambas as chamadas podem falhar. <code>requestAdapter()</code> retorna <code>null</code> quando o sistema não tem uma GPU utilizável, e <code>requestDevice()</code> pode ser rejeitada, portanto, trate essas possibilidades antes de continuar:</p>
<pre><code class="language-js">const adapter = await navigator.gpu.requestAdapter()
if (!adapter) throw new Error('No suitable GPU adapter found')
const device = await adapter.requestDevice()
</code></pre>
<p>O adaptador também é consumido quando você cria um dispositivo a partir dele. Portanto, solicite um novo adaptador se precisar de outro dispositivo.</p>
<h2>3) Configure o canvas</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')
const context = canvas.getContext('webgpu')
const format = navigator.gpu.getPreferredCanvasFormat()

context.configure({
  device,
  format,
  alphaMode: 'premultiplied',
})
</code></pre>
<h2>4) Escreva shaders em WGSL</h2>
<p>O WebGPU usa WGSL (WebGPU Shading Language), e não GLSL.</p>
<pre><code class="language-wgsl">@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -&gt; @builtin(position) vec4f {
  var pos = array&lt;vec2f, 3&gt;(
    vec2f( 0.0,  0.5),
    vec2f(-0.5, -0.5),
    vec2f( 0.5, -0.5),
  );
  return vec4f(pos[idx], 0.0, 1.0);
}

@fragment
fn fs_main() -&gt; @location(0) vec4f {
  return vec4f(1.0, 0.5, 0.2, 1.0); // orange
}
</code></pre>
<h2>5) Crie um módulo de shader</h2>
<pre><code class="language-js">const shaderModule = device.createShaderModule({
  code: wgslSource,
})
</code></pre>
<h2>6) Crie um pipeline de renderização</h2>
<pre><code class="language-js">const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: shaderModule,
    entryPoint: 'vs_main',
  },
  fragment: {
    module: shaderModule,
    entryPoint: 'fs_main',
    targets: [{ format }],
  },
})
</code></pre>
<h2>7) Renderize um quadro</h2>
<pre><code class="language-js">function render() {
  const commandEncoder = device.createCommandEncoder()
  const textureView = context.getCurrentTexture().createView()

  const renderPass = commandEncoder.beginRenderPass({
    colorAttachments: [{
      view: textureView,
      clearValue: { r: 0, g: 0, b: 0, a: 1 },
      loadOp: 'clear',
      storeOp: 'store',
    }],
  })

  renderPass.setPipeline(pipeline)
  renderPass.draw(3)
  renderPass.end()

  device.queue.submit([commandEncoder.finish()])
  requestAnimationFrame(render)
}
render()
</code></pre>
<h2>8) Buffers no WebGPU</h2>
<pre><code class="language-js">const vertices = new Float32Array([
  -0.5, -0.5,
   0.5, -0.5,
   0.0,  0.5,
])

const vertexBuffer = device.createBuffer({
  size: vertices.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
})

device.queue.writeBuffer(vertexBuffer, 0, vertices)
</code></pre>
<h2>9) Grupos de vinculação para uniforms</h2>
<pre><code class="language-js">const uniformBuffer = device.createBuffer({
  size: 64,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})

const bindGroup = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(0),
  entries: [{
    binding: 0,
    resource: { buffer: uniformBuffer },
  }],
})
</code></pre>
<h2>10) WebGPU vs. WebGL</h2>
<table>
<thead>
<tr>
<th>Aspecto</th>
<th>WebGL</th>
<th>WebGPU</th>
</tr>
</thead>
<tbody>
<tr>
<td>Estilo da API</td>
<td>Semelhante ao OpenGL, baseada em estado</td>
<td>Moderna, explícita</td>
</tr>
<tr>
<td>Shaders</td>
<td>GLSL</td>
<td>WGSL</td>
</tr>
<tr>
<td>Computação</td>
<td>Limitada (extensões)</td>
<td>Suporte de primeira classe</td>
</tr>
<tr>
<td>Multithreading</td>
<td>Difícil</td>
<td>Projetada para isso</td>
</tr>
<tr>
<td>Compatibilidade dos navegadores</td>
<td>Universal</td>
<td>Todos os principais navegadores (Chrome, Edge, Safari 26, Firefox)</td>
</tr>
</tbody>
</table>
<h2>Quando usar WebGPU</h2>
<ul>
<li>Novos projetos voltados para navegadores modernos</li>
<li>Cargas de trabalho com uso intensivo de computação (física, IA, geração procedural)</li>
<li>Quando você precisa de controle granular sobre os recursos da GPU</li>
</ul>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/webgl-fundamentals-for-games">Fundamentos do WebGL</a></li>
<li><a href="/pt-BR/tutorials/web-workers-game-logic">Web Workers para lógica de jogos</a></li>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rapidamente</a></li>
<li><a href="/pt-BR/guides/web-games-stack-2026">Stack tecnológico para jogos web em 2026</a> — critérios de decisão entre WebGL, WebGPU e Wasm</li>
<li><a href="/pt-BR/tutorials/coop-coep-sharedarraybuffer">COOP/COEP e SharedArrayBuffer</a> — cabeçalhos necessários para multithreading em Wasm com computação WebGPU</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://www.w3.org/TR/webgpu/">Especificação do WebGPU</a> — especificação do W3C</li>
<li><a href="https://www.w3.org/TR/WGSL/">Especificação da WGSL</a> — especificação da WebGPU Shading Language</li>
<li><a href="https://webgpufundamentals.org/">Fundamentos do WebGPU</a> — série de tutoriais aprofundados</li>
<li><a href="https://codelabs.developers.google.com/your-first-webgpu-app">Seu primeiro aplicativo WebGPU</a> — tutorial passo a passo do Google Codelab</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Fundamentos de multiplayer com WebSocket]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/websocket-multiplayer-basics</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/websocket-multiplayer-basics</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Crie experiências multiplayer em tempo real com WebSockets: gerenciamento de conexões, protocolos de mensagens e padrões de sincronização para jogos web.]]></description>
            <content:encoded><![CDATA[<h1>Fundamentos de multiplayer com WebSocket</h1>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0">
<iframe src="https://www.youtube.com/embed/cXxEiWudIUY" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture" allowfullscreen></iframe>
</div>
<p style="font-size:0.9rem;color:var(--vp-c-text-2);margin-top:-0.5rem">Criando um jogo multiplayer com WebSockets: arquitetura do servidor, salas de jogo e sincronização de estado</p>
<p>Os WebSockets permitem comunicação em tempo real para jogos multiplayer. Este tutorial aborda os fundamentos do lado do cliente.</p>
<h2>1) Conectando-se a um servidor</h2>
<pre><code class="language-js">const ws = new WebSocket('wss://game-server.example.com')

ws.onopen = () =&gt; {
  console.log('Connected')
  ws.send(JSON.stringify({ type: 'join', name: 'Player1' }))
}

ws.onmessage = (event) =&gt; {
  const msg = JSON.parse(event.data)
  handleMessage(msg)
}

ws.onclose = () =&gt; {
  console.log('Disconnected')
}

ws.onerror = (err) =&gt; {
  console.error('WebSocket error:', err)
}
</code></pre>
<h2>2) Estrutura do protocolo de mensagens</h2>
<p>Defina um protocolo simples:</p>
<pre><code class="language-js">// Client -&gt; Server
{ type: 'join', name: 'Player1' }
{ type: 'input', keys: { left: true, jump: true } }
{ type: 'chat', message: 'Hello!' }

// Server -&gt; Client
{ type: 'state', players: [...], entities: [...] }
{ type: 'playerJoined', id: 'abc', name: 'Player2' }
{ type: 'playerLeft', id: 'abc' }
</code></pre>
<h2>3) Classe de gerenciamento de conexão</h2>
<pre><code class="language-js">class GameConnection {
  constructor(url) {
    this.url = url
    this.ws = null
    this.handlers = new Map()
    this.reconnectDelay = 1000
  }
  
  connect() {
    this.ws = new WebSocket(this.url)
    
    this.ws.onopen = () =&gt; {
      this.reconnectDelay = 1000
      this.emit('connected')
    }
    
    this.ws.onmessage = (e) =&gt; {
      const msg = JSON.parse(e.data)
      this.emit(msg.type, msg)
    }
    
    this.ws.onclose = () =&gt; {
      this.emit('disconnected')
      this.scheduleReconnect()
    }
    
    this.ws.onerror = () =&gt; {
      this.ws.close()
    }
  }
  
  scheduleReconnect() {
    setTimeout(() =&gt; {
      this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
      this.connect()
    }, this.reconnectDelay)
  }
  
  send(type, data = {}) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, ...data }))
    }
  }
  
  on(type, handler) {
    if (!this.handlers.has(type)) {
      this.handlers.set(type, [])
    }
    this.handlers.get(type).push(handler)
  }
  
  emit(type, data) {
    const handlers = this.handlers.get(type) || []
    handlers.forEach(h =&gt; h(data))
  }
  
  disconnect() {
    this.ws?.close()
  }
}
</code></pre>
<h2>4) Enviando os comandos do jogador</h2>
<p>Envie comandos, não posições. Deixe o servidor ser a autoridade:</p>
<pre><code class="language-js">const connection = new GameConnection('wss://server.example.com')

function sendInput(input) {
  connection.send('input', {
    left: input.left,
    right: input.right,
    jump: input.jump,
    shoot: input.shoot,
    seq: inputSequence++,
  })
}

// Send input at a fixed rate (e.g., 20 times/sec)
setInterval(() =&gt; {
  sendInput(currentInput)
}, 50)
</code></pre>
<h2>5) Recebendo o estado do jogo</h2>
<pre><code class="language-js">let gameState = { players: [], entities: [] }

connection.on('state', (msg) =&gt; {
  gameState = msg
})

connection.on('playerJoined', (msg) =&gt; {
  gameState.players.push({ id: msg.id, name: msg.name })
})

connection.on('playerLeft', (msg) =&gt; {
  gameState.players = gameState.players.filter(p =&gt; p.id !== msg.id)
})
</code></pre>
<h2>6) Interpolação para movimentos suaves</h2>
<p>O servidor envia snapshots. Faça a interpolação entre eles:</p>
<pre><code class="language-js">class EntityInterpolator {
  constructor() {
    this.buffer = [] // { timestamp, state }
    this.renderDelay = 100 // ms behind server
  }
  
  addSnapshot(timestamp, state) {
    this.buffer.push({ timestamp, state })
    
    // Keep only recent snapshots
    while (this.buffer.length &gt; 10) {
      this.buffer.shift()
    }
  }
  
  getState(currentTime) {
    const renderTime = currentTime - this.renderDelay
    
    // Find surrounding snapshots
    let before = null
    let after = null
    
    for (let i = 0; i &lt; this.buffer.length - 1; i++) {
      if (this.buffer[i].timestamp &lt;= renderTime &amp;&amp; 
          this.buffer[i + 1].timestamp &gt;= renderTime) {
        before = this.buffer[i]
        after = this.buffer[i + 1]
        break
      }
    }
    
    if (!before || !after) {
      return this.buffer[this.buffer.length - 1]?.state
    }
    
    // Interpolate
    const t = (renderTime - before.timestamp) / (after.timestamp - before.timestamp)
    return this.lerp(before.state, after.state, t)
  }
  
  lerp(a, b, t) {
    return {
      x: a.x + (b.x - a.x) * t,
      y: a.y + (b.y - a.y) * t,
    }
  }
}
</code></pre>
<h2>7) Predição no lado do cliente</h2>
<p>Para controles responsivos, faça a predição localmente e reconcilie com o servidor:</p>
<pre><code class="language-js">class PredictedPlayer {
  constructor() {
    this.position = { x: 0, y: 0 }
    this.pendingInputs = []
  }
  
  applyInput(input) {
    // Apply input locally
    if (input.left) this.position.x -= 5
    if (input.right) this.position.x += 5
    
    // Store for reconciliation
    this.pendingInputs.push({ seq: input.seq, input })
  }
  
  reconcile(serverState, lastProcessedSeq) {
    // Server confirmed position
    this.position = { ...serverState.position }
    
    // Remove confirmed inputs
    this.pendingInputs = this.pendingInputs.filter(i =&gt; i.seq &gt; lastProcessedSeq)
    
    // Re-apply unconfirmed inputs
    for (const pending of this.pendingInputs) {
      if (pending.input.left) this.position.x -= 5
      if (pending.input.right) this.position.x += 5
    }
  }
}
</code></pre>
<h2>8) Exibindo a latência</h2>
<pre><code class="language-js">class LatencyMonitor {
  constructor(connection) {
    this.connection = connection
    this.pingStart = 0
    this.latency = 0
    
    connection.on('pong', () =&gt; {
      this.latency = Date.now() - this.pingStart
    })
    
    setInterval(() =&gt; this.ping(), 1000)
  }
  
  ping() {
    this.pingStart = Date.now()
    this.connection.send('ping')
  }
  
  getLatency() {
    return this.latency
  }
}
</code></pre>
<h2>9) Mensagens binárias para maior eficiência</h2>
<p>Para atualizações de alta frequência, use dados binários:</p>
<pre><code class="language-js">// Sending binary
const buffer = new ArrayBuffer(12)
const view = new DataView(buffer)
view.setFloat32(0, player.x)
view.setFloat32(4, player.y)
view.setUint32(8, inputFlags)
ws.send(buffer)

// Receiving binary
ws.binaryType = 'arraybuffer'
ws.onmessage = (e) =&gt; {
  if (e.data instanceof ArrayBuffer) {
    const view = new DataView(e.data)
    const x = view.getFloat32(0)
    const y = view.getFloat32(4)
    // ...
  }
}
</code></pre>
<h2>10) Exemplo básico de servidor (Node.js)</h2>
<pre><code class="language-js">import { WebSocketServer } from 'ws'

const wss = new WebSocketServer({ port: 8080 })
const players = new Map()

wss.on('connection', (ws) =&gt; {
  const id = crypto.randomUUID()
  players.set(id, { id, x: 0, y: 0, ws })
  
  ws.on('message', (data) =&gt; {
    const msg = JSON.parse(data)
    
    if (msg.type === 'input') {
      const player = players.get(id)
      if (msg.left) player.x -= 5
      if (msg.right) player.x += 5
    }
  })
  
  ws.on('close', () =&gt; {
    players.delete(id)
    broadcast({ type: 'playerLeft', id })
  })
})

// Broadcast state 20 times/sec
setInterval(() =&gt; {
  const state = {
    type: 'state',
    players: Array.from(players.values()).map(p =&gt; ({
      id: p.id, x: p.x, y: p.y
    }))
  }
  broadcast(state)
}, 50)

function broadcast(msg) {
  const data = JSON.stringify(msg)
  for (const player of players.values()) {
    player.ws.send(data)
  }
}
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/ship-web-game-fast">Publique um jogo web que carregue rápido</a></li>
<li><a href="/pt-BR/tutorials/game-input-handling">Gerenciamento de controles em jogos</a></li>
<li><a href="/pt-BR/tutorials/web-workers-game-logic">Web Workers para a lógica do jogo</a></li>
<li><a href="/pt-BR/tutorials/coop-coep-sharedarraybuffer">COOP/COEP e SharedArrayBuffer</a> — cabeçalhos necessários para usar SharedArrayBuffer em contextos multiplayer</li>
<li><a href="/pt-BR/guides/co-op-game-design">Design de jogos cooperativos em 2026</a> — por que jogos cooperativos do tipo &quot;friend slop&quot; estão em alta e como o multiplayer no navegador se encaixa nisso</li>
<li><a href="/pt-BR/guides/game-jams-hackathons">Game jams e hackathons</a> — jogos multiplayer de jams recebem mais atenção</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API">MDN: API WebSocket</a> — referência sobre WebSocket para navegadores</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API">MDN: API WebRTC</a> — conexões ponto a ponto para reduzir a latência</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API">MDN: API WebTransport</a> — um transporte HTTP/3 mais recente que passou a fazer parte do Baseline em todos os navegadores em março de 2026, quando o Safari 26.4 recebeu suporte (ele já estava disponível no Chrome 97+, Edge e Firefox 114+). Ao contrário de um único WebSocket baseado em TCP, o WebTransport funciona sobre QUIC e oferece datagramas não confiáveis, além de vários fluxos independentes. Assim, um pacote perdido não bloqueia todo o restante da fila. Um padrão comum é enviar atualizações de posição de alta frequência por datagramas e manter o chat ou os placares em um fluxo confiável. Os WebSockets ainda são a opção mais compatível, e tudo o que foi apresentado acima continua válido, mas agora vale a pena considerar o WebTransport para jogos sensíveis à latência.</li>
<li><a href="https://socket.io/docs/v4/">Documentação do Socket.IO</a> — biblioteca popular que encapsula WebSockets</li>
<li><a href="https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking">Redes multiplayer do Valve Source</a> — padrões de rede autoritativa usados em jogos em produção</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Noções básicas de WebXR para jogos de RV]]></title>
            <link>https://app.cinevva.com/pt-BR/tutorials/webxr-basics</link>
            <guid isPermaLink="true">https://app.cinevva.com/pt-BR/tutorials/webxr-basics</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Comece a usar WebXR: configuração de sessão, renderização, controles e interações. Crie experiências de RV que rodam no navegador.]]></description>
            <content:encoded><![CDATA[<h1>Noções básicas de WebXR para jogos de RV</h1>
<p>O WebXR leva RV e RA ao navegador. Os jogadores podem aproveitar seu jogo em um headset sem precisar baixar nada.</p>
<h2>1) Verifique o suporte a WebXR</h2>
<pre><code class="language-js">async function checkXRSupport() {
  if (!navigator.xr) {
    return { vr: false, ar: false }
  }
  
  const vr = await navigator.xr.isSessionSupported('immersive-vr')
  const ar = await navigator.xr.isSessionSupported('immersive-ar')
  
  return { vr, ar }
}

// Usage
const support = await checkXRSupport()
if (support.vr) {
  showVRButton()
}
</code></pre>
<h2>2) Iniciando uma sessão de RV</h2>
<pre><code class="language-js">let xrSession = null
let xrRefSpace = null

async function startVR() {
  try {
    xrSession = await navigator.xr.requestSession('immersive-vr', {
      requiredFeatures: ['local-floor'],
      optionalFeatures: ['hand-tracking'],
    })
    
    xrSession.addEventListener('end', onSessionEnd)
    
    // Set up rendering
    const gl = canvas.getContext('webgl2', { xrCompatible: true })
    await xrSession.updateRenderState({
      baseLayer: new XRWebGLLayer(xrSession, gl),
    })
    
    // Get reference space
    xrRefSpace = await xrSession.requestReferenceSpace('local-floor')
    
    // Start render loop
    xrSession.requestAnimationFrame(onXRFrame)
  } catch (err) {
    console.error('Failed to start VR:', err)
  }
}

function onSessionEnd() {
  xrSession = null
  xrRefSpace = null
}
</code></pre>
<h2>3) O loop de renderização XR</h2>
<pre><code class="language-js">function onXRFrame(time, frame) {
  const session = frame.session
  session.requestAnimationFrame(onXRFrame)
  
  const pose = frame.getViewerPose(xrRefSpace)
  if (!pose) return
  
  const glLayer = session.renderState.baseLayer
  gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer)
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  
  // Render for each eye
  for (const view of pose.views) {
    const viewport = glLayer.getViewport(view)
    gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height)
    
    // Get camera matrices
    const viewMatrix = view.transform.inverse.matrix
    const projectionMatrix = view.projectionMatrix
    
    // Render scene with these matrices
    renderScene(viewMatrix, projectionMatrix)
  }
}
</code></pre>
<h2>4) Fontes de entrada (controles)</h2>
<pre><code class="language-js">xrSession.addEventListener('inputsourceschange', (e) =&gt; {
  for (const source of e.added) {
    console.log('Controller added:', source.handedness) // 'left' or 'right'
  }
  for (const source of e.removed) {
    console.log('Controller removed:', source.handedness)
  }
})

function processInput(frame) {
  for (const source of xrSession.inputSources) {
    if (!source.gamepad) continue
    
    const gamepad = source.gamepad
    
    // Trigger (usually index 0)
    const triggerPressed = gamepad.buttons[0]?.pressed
    const triggerValue = gamepad.buttons[0]?.value || 0
    
    // Grip (usually index 1)
    const gripPressed = gamepad.buttons[1]?.pressed
    
    // Thumbstick
    const thumbstickX = gamepad.axes[2] || 0
    const thumbstickY = gamepad.axes[3] || 0
    
    // A/B buttons (index 4, 5)
    const buttonA = gamepad.buttons[4]?.pressed
    const buttonB = gamepad.buttons[5]?.pressed
  }
}
</code></pre>
<h3>Não presuma que todo dispositivo tenha um gamepad</h3>
<p>O código acima lê <code>source.gamepad</code> para gatilhos e direcionais analógicos. Isso funciona em headsets com controles, como o Meta Quest, mas não faz nada no Apple Vision Pro (o WebXR vem ativado por padrão no Safari do visionOS 2), no Samsung Galaxy XR e em outros dispositivos Android XR, nos quais a principal forma de entrada é olhar e pinçar ou o rastreamento das mãos. Nesses headsets, <code>source.gamepad</code> é nulo, e <code>session.inputSources</code> permanece vazio até o usuário realmente fazer o gesto de pinça.</p>
<p>Para a única interação garantida por todos os dispositivos, a “ação principal”, escute os eventos <code>select</code> em vez de verificar continuamente os botões. O WebXR fornece <code>selectstart</code>, <code>select</code> e <code>selectend</code> tanto para o gatilho de um controle quanto para uma pinça com a mão ou o gesto de olhar e pinçar do Vision Pro. Assim, um único manipulador atende a todos eles:</p>
<pre><code class="language-js">xrSession.addEventListener('selectstart', (e) =&gt; {
  // e.inputSource is the controller, hand, or transient pinch
  // e.frame lets you read its targetRaySpace pose
})
xrSession.addEventListener('select', (e) =&gt; {
  // primary action completed (trigger pulled, pinch released on target)
})
xrSession.addEventListener('selectend', (e) =&gt; {})
</code></pre>
<p>Mantenha o código do gamepad para locomoção com o direcional analógico e botões extras em headsets com controles, mas condicione sua execução a <code>if (source.gamepad)</code> e encaminhe a interação principal de “selecionar/agarrar/ativar” por esses eventos. Assim, o jogo continuará funcionando em dispositivos controlados pelas mãos e pelo olhar.</p>
<h2>5) Posições dos controles</h2>
<pre><code class="language-js">function getControllerPose(frame, source) {
  if (!source.gripSpace) return null
  
  const pose = frame.getPose(source.gripSpace, xrRefSpace)
  if (!pose) return null
  
  return {
    position: pose.transform.position,
    orientation: pose.transform.orientation,
    matrix: pose.transform.matrix,
  }
}

function renderControllers(frame) {
  for (const source of xrSession.inputSources) {
    const pose = getControllerPose(frame, source)
    if (pose) {
      renderControllerModel(pose, source.handedness)
    }
  }
}
</code></pre>
<h2>6) Ray casting para apontar</h2>
<pre><code class="language-js">function getControllerRay(frame, source) {
  if (!source.targetRaySpace) return null
  
  const pose = frame.getPose(source.targetRaySpace, xrRefSpace)
  if (!pose) return null
  
  const origin = pose.transform.position
  const direction = {
    x: -pose.transform.matrix[8],
    y: -pose.transform.matrix[9],
    z: -pose.transform.matrix[10],
  }
  
  return { origin, direction }
}

function checkRayIntersection(ray, objects) {
  let closest = null
  let closestDist = Infinity
  
  for (const obj of objects) {
    const dist = rayIntersectBox(ray, obj.boundingBox)
    if (dist !== null &amp;&amp; dist &lt; closestDist) {
      closest = obj
      closestDist = dist
    }
  }
  
  return closest
}
</code></pre>
<h2>7) Locomoção por teletransporte</h2>
<pre><code class="language-js">class TeleportSystem {
  constructor() {
    this.targetPosition = null
    this.isAiming = false
  }
  
  update(frame, inputSources) {
    for (const source of inputSources) {
      if (source.handedness !== 'left') continue
      
      const gamepad = source.gamepad
      const thumbstickY = gamepad?.axes[3] || 0
      
      if (thumbstickY &lt; -0.5) {
        // Aiming
        this.isAiming = true
        const ray = getControllerRay(frame, source)
        this.targetPosition = this.findTeleportTarget(ray)
      } else if (this.isAiming) {
        // Release - teleport
        if (this.targetPosition) {
          player.position.x = this.targetPosition.x
          player.position.z = this.targetPosition.z
        }
        this.isAiming = false
        this.targetPosition = null
      }
    }
  }
  
  findTeleportTarget(ray) {
    // Intersect with ground plane
    if (ray.direction.y &gt;= 0) return null
    
    const t = -ray.origin.y / ray.direction.y
    if (t &lt; 0 || t &gt; 10) return null
    
    return {
      x: ray.origin.x + ray.direction.x * t,
      y: 0,
      z: ray.origin.z + ray.direction.z * t,
    }
  }
  
  render() {
    if (this.isAiming &amp;&amp; this.targetPosition) {
      renderTeleportMarker(this.targetPosition)
    }
  }
}
</code></pre>
<h2>8) Locomoção suave</h2>
<pre><code class="language-js">function updateSmoothLocomotion(frame, inputSources, dt) {
  for (const source of inputSources) {
    if (source.handedness !== 'left') continue
    
    const gamepad = source.gamepad
    if (!gamepad) continue
    
    const moveX = gamepad.axes[2] || 0
    const moveY = gamepad.axes[3] || 0
    
    // Apply deadzone
    if (Math.abs(moveX) &lt; 0.1) moveX = 0
    if (Math.abs(moveY) &lt; 0.1) moveY = 0
    
    // Get head direction for movement
    const pose = frame.getViewerPose(xrRefSpace)
    if (!pose) continue
    
    const headMatrix = pose.transform.matrix
    const forward = { x: -headMatrix[8], z: -headMatrix[10] }
    const right = { x: headMatrix[0], z: headMatrix[2] }
    
    // Normalize to XZ plane
    const len = Math.sqrt(forward.x ** 2 + forward.z ** 2)
    forward.x /= len
    forward.z /= len
    
    const speed = 3 * dt
    player.position.x += (forward.x * -moveY + right.x * moveX) * speed
    player.position.z += (forward.z * -moveY + right.z * moveX) * speed
  }
}
</code></pre>
<h2>9) Diretrizes de conforto em RV</h2>
<pre><code class="language-js">// Vignette during movement to reduce motion sickness
function renderComfortVignette(movementSpeed) {
  const intensity = Math.min(movementSpeed / 5, 0.5)
  if (intensity &lt; 0.1) return
  
  // Render dark edges that increase with speed
  renderVignette(intensity)
}

// Snap turning
let snapTurnCooldown = 0

function handleSnapTurn(gamepad, dt) {
  snapTurnCooldown -= dt
  
  const thumbstickX = gamepad.axes[2] || 0
  
  if (Math.abs(thumbstickX) &gt; 0.7 &amp;&amp; snapTurnCooldown &lt;= 0) {
    const angle = Math.sign(thumbstickX) * (Math.PI / 4) // 45 degrees
    player.rotation += angle
    snapTurnCooldown = 0.3 // Prevent rapid turns
  }
}
</code></pre>
<h2>10) Configuração completa do WebXR</h2>
<pre><code class="language-js">class VRGame {
  constructor(canvas) {
    this.canvas = canvas
    this.gl = canvas.getContext('webgl2', { xrCompatible: true })
    this.session = null
    this.refSpace = null
    this.teleport = new TeleportSystem()
  }
  
  async checkSupport() {
    if (!navigator.xr) return false
    return await navigator.xr.isSessionSupported('immersive-vr')
  }
  
  async start() {
    this.session = await navigator.xr.requestSession('immersive-vr', {
      requiredFeatures: ['local-floor'],
    })
    
    await this.session.updateRenderState({
      baseLayer: new XRWebGLLayer(this.session, this.gl),
    })
    
    this.refSpace = await this.session.requestReferenceSpace('local-floor')
    this.session.addEventListener('end', () =&gt; this.onEnd())
    this.session.requestAnimationFrame((t, f) =&gt; this.onFrame(t, f))
  }
  
  onFrame(time, frame) {
    this.session.requestAnimationFrame((t, f) =&gt; this.onFrame(t, f))
    
    const dt = (time - this.lastTime) / 1000
    this.lastTime = time
    
    // Update game logic
    this.teleport.update(frame, this.session.inputSources)
    
    // Render
    const pose = frame.getViewerPose(this.refSpace)
    if (!pose) return
    
    const glLayer = this.session.renderState.baseLayer
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, glLayer.framebuffer)
    this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT)
    
    for (const view of pose.views) {
      const vp = glLayer.getViewport(view)
      this.gl.viewport(vp.x, vp.y, vp.width, vp.height)
      this.render(view.transform.inverse.matrix, view.projectionMatrix)
    }
    
    this.teleport.render()
  }
  
  render(viewMatrix, projectionMatrix) {
    // Your rendering code here
  }
  
  onEnd() {
    this.session = null
  }
}
</code></pre>
<h2>Conteúdo relacionado</h2>
<ul>
<li><a href="/pt-BR/tutorials/webgl-fundamentals-for-games">Fundamentos de WebGL</a></li>
<li><a href="/pt-BR/tutorials/webgpu-getting-started">Primeiros passos com WebGPU</a></li>
<li><a href="/pt-BR/tutorials/game-input-handling">Gerenciamento de entrada em jogos</a></li>
<li><a href="/pt-BR/guides/web-games-stack-2026">Stack tecnológico para jogos web em 2026</a> — onde o WebXR se encaixa no panorama tecnológico mais amplo</li>
<li><a href="/pt-BR/guides/threejs-usdc-tech-report">Three.js + USDC no navegador</a> — como carregar recursos 3D para cenas de XR</li>
</ul>
<h2>Recursos externos</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API">MDN: API WebXR Device</a> — referência completa da API</li>
<li><a href="https://immersiveweb.dev/">Immersive Web</a> — exemplos de WebXR, ferramentas e tabelas de compatibilidade com navegadores</li>
<li><a href="https://threejs.org/docs/#manual/en/introduction/How-to-create-VR-content">Guia de WebXR do Three.js</a> — conteúdo de RV com Three.js</li>
</ul>
]]></content:encoded>
        </item>
    </channel>
</rss>
