Estrutura de projeto de mini game no Cocos Creator: Boot, cena e tela de resultado

Encarando o erro “scene not found” na tela, percebi que a estrutura de diretórios daquele mini game já tinha saído completamente do controle.
Três meses antes, comecei o projeto cheio de confiança: uma cena para o menu, uma cena para a interface principal do jogo, outra cena para a tela de resultado. Soava bem razoável, certo? Na prática, a pontuação do jogador sumia sem explicação durante a troca de cena, a animação de loading travava como uma apresentação de slides, e o pior: uma fase levava 5 segundos para carregar. Metade dos usuários abandonava antes mesmo de ver a tela do jogo.
Foi aí que caiu a ficha: arquitetura de mini game não se resolve empilhando alguns arquivos scene. Depois, refatorei o projeto inteiro para uma arquitetura de cena única + quatro Layers. O tempo de carregamento caiu de 5 segundos para menos de 2, e a troca de telas ficou lisa de verdade.
Por que mini games costumam usar arquitetura de cena única
Sendo bem honesto, quando comecei a fazer mini games, eu nem pensava em “arquitetura”. Menu, jogo, resultado: uma cena separada para cada interface. Simples e direto.
Só que, ao rodar o projeto, os problemas apareceram em sequência:
Custo de troca. A cada director.loadScene(), o engine precisa destruir a cena antiga, criar a nova e recarregar recursos. Para mini games, isso é um desastre: 53% dos usuários móveis abandonam páginas que carregam por mais de 3 segundos, e a espera causada por múltiplas trocas de cena pode ultrapassar esse limite com facilidade.
Perda de estado. O jogador termina uma fase, faz 1200 pontos, vai para a tela de resultado… e os dados somem. Por quê? Porque a troca de cena destrói todos os nós, a menos que você salve os dados em uma variável global ou em um nó persistente. Mais adiante eu mostro como fazer isso.
Interrupção de animações. O menu ainda está exibindo uma animação legal do logo, o usuário toca no botão de iniciar, a cena troca… e a animação é cortada de forma brusca. A sensação fica péssima.
A vantagem da arquitetura de cena única está justamente nisso: todas as camadas de UI ficam dentro da mesma cena, e trocar de tela significa apenas alterar a propriedade active de um Layer. Não há custo de destruir e recriar, o estado é preservado naturalmente e as animações não são interrompidas. Mini games normalmente têm poucas interfaces, então essa arquitetura dá conta muito bem. Para jogos maiores, a conversa muda: muitas fases e recursos pesados realmente pedem múltiplas cenas e carregamento por pacotes. Mas para mini game? Uma cena costuma bastar.
Modelo de estrutura de diretórios do projeto
Beleza, já sabemos por que usar uma arquitetura de cena única. Agora vem a pergunta prática: como organizar os diretórios do projeto?
Este é o modelo que consolidei depois de cair em várias armadilhas, usando Cocos Creator 3.x como referência:
assets/
├── Scenes/
│ └── Main.scene # Cena principal única
├── Scripts/
│ ├── managers/
│ │ ├── GameManager.ts # Estado do jogo e armazenamento de dados
│ │ ├── UIManager.ts # Lógica de troca de Layers
│ │ └── AudioManager.ts # Gerenciamento de áudio
│ ├── layers/
│ │ ├── BootLayer.ts # Lógica da tela de boot
│ │ ├── MenuLayer.ts # Lógica do menu principal
│ │ ├── GameLayer.ts # Interface principal do jogo
│ │ └── SettlementLayer.ts # Tela de resultado
│ └── components/
│ ├── PlayerController.ts
│ └── EnemyAI.ts
├── Prefabs/
│ ├── UI/
│ │ ├── BootPanel.prefab # Prefab de UI da tela de boot
│ │ ├── MenuPanel.prefab # UI do menu
│ │ ├── SettlementPanel.prefab
│ ├── Game/
│ │ ├── Player.prefab
│ │ ├── Obstacle.prefab
│ └── Effects/
│ └── Explosion.prefab
├── Resources/
│ ├── textures/ # Imagens carregadas dinamicamente
│ ├── audio/ # Arquivos de áudio
│ └── fonts/
└── bundles/ # Subpacotes de Asset Bundle para otimizar mini games do WeChat
├── core/ # Pacote de recursos essenciais
└── levels/ # Pacote de recursos de fases
Alguns pontos importantes:
O diretório Scenes deve ter apenas um arquivo. Main.scene é a única cena principal. Dentro dela ficam o Canvas, o GameManager e todos os Layers. Não continue colocando outros arquivos scene nesse diretório.
managers guarda scripts de gerenciamento. O GameManager é configurado como nó persistente com game.addPersistRootNode() e armazena dados como pontuação e progresso de fases; o UIManager cuida da lógica de troca entre Layers.
layers guarda os scripts de lógica de cada camada de UI. Cada Layer corresponde a um Prefab, dentro de Prefabs/UI. O script é anexado ao Prefab, que depois é arrastado para baixo do nó Canvas em Main.scene.
bundles é preparado para mini games de WeChat/Douyin. Mini games têm limite de tamanho no pacote inicial; tudo que passar de 4MB deve ser carregado por Asset Bundle. Os recursos essenciais ficam no bundle core, e os recursos de fases são carregados sob demanda.
O seu projeto está organizado assim? Se não estiver, talvez valha ajustar antes que a estrutura fique difícil de manter.
Cena de Boot: como desenhar a tela de inicialização
Primeiro, um dado: o próprio engine Cocos Creator tem cerca de 1.9MB. Somando os recursos do seu jogo, o tempo padrão de carregamento da primeira tela costuma ficar em 3 a 5 segundos. Para mini games, isso é tempo demais. O usuário abre o jogo, encara uma tela preta ou vazia por 3 segundos, e muita gente simplesmente fecha.
O Boot Layer existe para resolver esse problema. As responsabilidades dele são bem simples:
Mostrar o progresso de carregamento. O usuário precisa saber que o jogo está carregando, não travado. Uma barra de progresso ou um número em porcentagem já resolve; não invente algo visualmente complexo demais, porque nesse momento as imagens ainda nem terminaram de carregar.
Pré-carregar recursos essenciais. Use resources.preload() ou loadBundle() do Asset Bundle para carregar antes as imagens e áudios necessários logo em seguida.
Exibir a marca. Coloque um logo e uma animação simples, como um scale com fade-in, aproveitando para reforçar a marca.
O fluxo de inicialização fica mais ou menos assim:
Inicialização do engine → Exibição do Boot Layer → Preload de recursos essenciais → Carregamento concluído → Troca para MenuLayer
Exemplo de código em BootLayer.ts:
import { _decorator, Component, Node, resources, ProgressBar } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('BootLayer')
export class BootLayer extends Component {
@property(ProgressBar)
progressBar: ProgressBar | null = null;
start() {
// Pré-carrega recursos essenciais
this.preloadCoreAssets();
}
preloadCoreAssets() {
resources.preloadDir('textures', (err, items) => {
if (err) {
console.error('Falha no preload:', err);
return;
}
// Carregamento concluído; troca para o menu
this.switchToMenu();
});
}
updateProgress(current: number, total: number) {
if (this.progressBar) {
this.progressBar.progress = current / total;
}
}
switchToMenu() {
// Avisa o UIManager para trocar para o MenuLayer
// O código será mostrado mais adiante
}
}
Otimização do pacote inicial no WeChat mini game: se o jogo for publicado na plataforma de mini games do WeChat, o limite do pacote inicial é 4MB. O que passar disso precisa ir para subpacotes com Asset Bundle. Esses bundles podem ser carregados no Boot Layer:
assetManager.loadBundle('levels', (err, bundle) => {
if (err) return;
console.log('Pacote de fases carregado');
});
Tela de inicialização personalizada no H5: o Cocos Creator oferece o mecanismo build-templates, que permite personalizar a tela de inicialização depois do build para H5. Crie o diretório build-templates/web-mobile na raiz do projeto, coloque um index.html personalizado ali e você consegue substituir a tela preta padrão por uma animação de loading ou uma imagem da marca.
Para os detalhes, veja o tutorial no fórum oficial: Creator | página de inicialização personalizada em H5.
Com o Boot Layer definido, chegamos ao núcleo da arquitetura: como alternar entre os quatro Layers.
Implementação da arquitetura em quatro Layers: do menu ao resultado
Esta é a parte mais importante. A estrutura da cena principal fica assim:
Main.scene
├── Canvas (contêiner de UI)
│ ├── BootLayer → progresso de carregamento e exibição da marca
│ ├── MenuLayer → tela inicial e seleção de fases
│ ├── GameLayer → interface principal do jogo
│ └── SettlementLayer → tela de resultado (pontuação, tempo, continuar/tentar de novo)
├── GameManager (nó persistente)
│ └─ Armazena: pontuação, progresso de fase, configurações do jogador
└── AudioRoot (nó de gerenciamento de áudio)
Repare: os quatro Layers ficam dentro do mesmo Canvas. Na hora de trocar de tela, você só altera a propriedade active de um Layer. Os outros Layers continuam existindo; o estado não se perde e as animações não são cortadas.
A lógica principal de troca de Layers fica no UIManager:
// UIManager.ts
import { _decorator, Component, Node, tween, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('UIManager')
export class UIManager extends Component {
private layers: Map<string, Node> = new Map();
private currentLayer: string = 'BootLayer';
onLoad() {
// Coleta todos os nós de Layer
const canvas = this.node.getChildByName('Canvas');
if (!canvas) return;
canvas.children.forEach(child => {
if (child.name.endsWith('Layer')) {
this.layers.set(child.name, child);
child.active = false; // Esconde tudo no início
}
});
// Exibe o BootLayer
this.switchLayer('BootLayer');
}
switchLayer(targetLayer: string) {
// Esconde a camada atual
const current = this.layers.get(this.currentLayer);
if (current) {
current.active = false;
}
// Exibe a camada de destino
const target = this.layers.get(targetLayer);
if (target) {
target.active = true;
// Adiciona uma animação de fade-in (opcional)
this.playFadeIn(target);
}
this.currentLayer = targetLayer;
}
playFadeIn(node: Node) {
// Animação simples de scale com fade-in visual
node.setScale(new Vec3(0.9, 0.9, 1));
tween(node)
.to(0.2, { scale: new Vec3(1, 1, 1) })
.start();
}
}
Ponto chave: use um Map para armazenar as referências de todos os Layers. Assim, durante a troca, você não precisa chamar getChildByName() toda vez. Fica mais eficiente e mais previsível.
Passagem de dados para a tela de resultado: o jogador termina uma fase, faz 1200 pontos e leva 45 segundos. Como esses dados chegam à tela de resultado?
A resposta é: GameManager.
O GameManager é um nó persistente, com ciclo de vida durante todo o jogo. Quando o jogador termina a fase no GameLayer, você salva a pontuação e o tempo no GameManager. Ao trocar para o SettlementLayer, lê esses dados do GameManager e mostra na interface.
// GameManager.ts (versão simplificada)
import { _decorator, Component, game } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('GameManager')
export class GameManager extends Component {
public currentScore: number = 0;
public currentLevel: number = 1;
public playTime: number = 0;
onLoad() {
// Define como nó persistente
game.addPersistRootNode(this.node);
}
// GameLayer chama este método para salvar dados
saveResult(score: number, time: number) {
this.currentScore = score;
this.playTime = time;
}
// SettlementLayer chama este método para ler dados
getResult() {
return {
score: this.currentScore,
time: this.playTime
};
}
}
Quando o SettlementLayer é exibido, ele lê os dados diretamente do GameManager:
// SettlementLayer.ts
onEnable() {
const gameManager = find('GameManager')?.getComponent(GameManager);
if (!gameManager) return;
const result = gameManager.getResult();
this.scoreLabel.string = `Pontuação: ${result.score}`;
this.timeLabel.string = `Tempo: ${result.time}s`;
}
Com isso, a passagem de dados fica resolvida. Não precisa de um sistema de eventos complexo; o GameManager segura o que precisa ser compartilhado.
Nó persistente: passagem de dados entre Layers
Já falei de nó persistente acima, mas vale detalhar um pouco mais.
game.addPersistRootNode() é uma API oficial do Cocos Creator. Ela faz com que um nó não seja destruído durante a troca de cena. Mas, se estamos usando arquitetura de cena única e a cena não troca, por que ainda usar um nó persistente?
Porque ele continua útil dentro da arquitetura de cena única: pode funcionar como centro global de dados e eventos.
Responsabilidades do GameManager:
Armazenar dados do jogador: pontuação, progresso de fases, recorde, lista de fases desbloqueadas.
Armazenar configurações: som ligado/desligado, música ligada/desligada, idioma.
Fornecer eventos globais: por exemplo, um evento de “fase concluída” que diferentes Layers podem escutar e responder.
Cuidados importantes:
O nó persistente não deve ficar dentro do Canvas. O Canvas é um contêiner de UI e pode ser manipulado conforme o estado active dos Layers muda. O nó persistente deve ser independente, no nível raiz da cena.
O nó persistente precisa ser criado manualmente. Em Main.scene, crie um nó vazio chamado “GameManager”, anexe o script GameManager e chame game.addPersistRootNode(this.node) dentro de onLoad().
Use o nó persistente com cuidado. Não enfie tudo nele. Salve apenas os dados que realmente precisam ser compartilhados entre interfaces. O estado de UI de cada Layer, como se um botão deve aparecer ou não, deve ficar no próprio Layer.
Um exemplo de estrutura de dados completa:
// GameManager.ts (versão completa)
interface PlayerData {
highestScore: number;
unlockedLevels: number[];
currentLevel: number;
}
interface GameSettings {
soundEnabled: boolean;
musicEnabled: boolean;
language: 'zh' | 'en';
}
@ccclass('GameManager')
export class GameManager extends Component {
private playerData: PlayerData = {
highestScore: 0,
unlockedLevels: [1],
currentLevel: 1
};
private settings: GameSettings = {
soundEnabled: true,
musicEnabled: true,
language: 'zh'
};
onLoad() {
game.addPersistRootNode(this.node);
// Lê os dados do armazenamento local
this.loadData();
}
loadData() {
const saved = localStorage.getItem('playerData');
if (saved) {
this.playerData = JSON.parse(saved);
}
}
saveData() {
localStorage.setItem('playerData', JSON.stringify(this.playerData));
}
// getters e setters...
}
Aqui também entra uma leitura e escrita com localStorage, persistindo os dados do jogador localmente. Plataformas de mini games costumam oferecer suporte a localStorage. No WeChat, por exemplo, a API nativa é wx.setStorageSync, mas um localStorage encapsulado também funciona.
Resumo
Depois de tudo isso, fica uma checklist rápida de decisão arquitetural para comparar com o seu projeto:
Se você está criando um mini game:
- Use arquitetura de cena única, com um
Main.scene - Coloque todas as camadas de UI dentro do Canvas e alterne com Layers
- Deixe o BootLayer cuidar do progresso de carregamento e do preload
- Use o GameManager como nó persistente para armazenar dados
- Organize os diretórios por assets/Scripts/managers/layers/Prefabs
Se você está criando um jogo maior:
- Múltiplas cenas podem ser necessárias, principalmente com muitas fases e recursos pesados
- Mesmo assim, a camada de UI pode aproveitar a ideia de cena única + Layer
- Use Asset Bundle para carregamento por subpacotes
Usei essa arquitetura em alguns projetos de mini game, depois de cair em armadilhas e refatorar algumas vezes. Hoje ela me parece madura o suficiente para servir como base. Se tiver algum problema, dá para deixar um comentário ou procurar um projeto de exemplo no GitHub para comparar.
Na próxima, quero falar dos efeitos de animação para troca de Layers: fade-in, fade-out, transição deslizante e scale, para deixar as mudanças ainda mais fluidas.
Montar uma arquitetura de cena única para mini games no Cocos Creator
Como criar do zero uma estrutura de mini game com Boot, cena principal e tela de resultado
⏱️ Estimated time: 30 min
- 1
Step 1: Criar a estrutura de diretórios do projeto
Dentro de assets, crie diretórios como Scenes, Scripts/managers, Scripts/layers, Scripts/components, Prefabs/UI, Prefabs/Game, Resources e bundles, separando os arquivos por responsabilidade. - 2
Step 2: Criar Main.scene e GameManager
Crie a única cena principal, Main.scene; no nível raiz, crie o nó GameManager, anexe o script e chame game.addPersistRootNode(this.node) em onLoad() para marcá-lo como nó persistente. - 3
Step 3: Criar quatro prefabs de Layer
Crie os quatro Prefabs BootLayer, MenuLayer, GameLayer e SettlementLayer; cada Prefab deve receber seu script de lógica correspondente e ficar dentro de Prefabs/UI. - 4
Step 4: Implementar a lógica de troca no UIManager
Em UIManager.ts, use um Map para armazenar as referências de todos os Layers e implemente switchLayer(targetLayer), alternando a interface por meio da propriedade active e adicionando uma animação de fade-in para melhorar a experiência. - 5
Step 5: Implementar o fluxo de carregamento do Boot Layer
Em BootLayer.ts, chame resources.preloadDir() para pré-carregar recursos essenciais, atualize a barra de progresso e, ao terminar, chame o UIManager para trocar para o MenuLayer.
FAQ
Por que mini games costumam usar arquitetura de cena única?
Como passar dados durante a troca de telas?
Em qual nível o nó persistente deve ficar?
Qual é o papel do Boot Layer?
O que fazer se o pacote inicial de um mini game do WeChat passar de 4MB?
12 min de leitura · Publicado em: 19 mai 2026 · Atualizado em: 14 jul 2026
Desenvolvimento de mini games Cocos com IA
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Desenvolvedor indie criando jogo pequeno: valide a jogabilidade antes de empilhar sistemas
O erro mais comum de quem cria jogos pequenos como indie é construir sistemas demais e só depois descobrir que o jogo não diverte. Veja um caminho prático de validação de MVP, do Core Loop ao teste de protótipo, para decidir com pouco custo se vale continuar.
Parte 2 de 21
Próximo
Design de máquina de estados para minijogos: fluxo completo da tela inicial à batalha e ao resultado
Entenda a arquitetura de máquina de estados em três camadas para minijogos, do fluxo principal aos detalhes da batalha. Inclui interfaces em TypeScript, caso prático com Cocos Creator e soluções para desvio de estado e conflitos de concorrência.
Parte 4 de 21



Comentários
Entre com GitHub para comentar