Cambiar tema

Estructura de proyecto de minijuegos en Cocos Creator: Boot, escenas y pantalla de resultados

Easton editorial illustration: one Main-scene base carrying four stacked interface layers
1.9MB
Tamaño de carga del motor
Tamaño del motor Cocos Creator en sí
53%
Tasa de abandono
Proporción de usuarios móviles que abandonan si la carga supera 3 segundos
Menos de 2 s
Tiempo de carga optimizado
Tiempo de primera pantalla tras optimizar con arquitectura de escena única
数据来源: Blog de CSDN, casos de Tencent Cloud

Mirando el error “scene not found” en pantalla, me di cuenta de que la estructura de directorios de este minijuego ya se había ido por completo de las manos.

Hace tres meses empecé con confianza: un escenario para el menú, otro para el juego principal y otro para los resultados. Suena razonable, ¿no? Pues no: al cambiar de escena la puntuación del jugador desaparecía sin motivo, la animación de carga se trababa como un PowerPoint y lo peor, cargar un nivel podía tardar 5 segundos; la mitad de los usuarios se iba antes de ver el juego.

Entonces entendí que la arquitectura de un minijuego no consiste en apilar archivos .scene al azar. Reestructuré todo con escena única + cuatro Layer; el tiempo de carga pasó de 5 segundos a menos de 2 y el cambio entre pantallas quedó tan fluido como mantequilla.

Por qué se recomienda la arquitectura de escena única

Al principio ni pensaba en “arquitectura”. Menú, juego, resultados: cada pantalla en su propio .scene, simple y directo.

Al ejecutarlo, los problemas aparecieron en cascada:

Costo del cambio. Cada director.loadScene() obliga al motor a destruir la escena anterior, crear la nueva y volver a cargar recursos. Para un minijuego es un desastre: el 53% de usuarios móviles abandona si la carga supera 3 segundos, y con varias escenas la espera supera ese umbral con facilidad.

Pérdida de estado. El jugador termina un nivel con 1200 puntos, cambias a la pantalla de resultados y los datos desaparecen. ¿Por qué? El cambio de escena destruye todos los nodos, salvo que guardes datos en variables globales o en un nodo persistente (más adelante explico cómo).

Animaciones interrumpidas. Un logo animado en el menú aún no termina, el usuario pulsa Iniciar, cambias de escena y la animación se corta en seco: mala experiencia.

La ventaja de la escena única es clara: todas las capas de UI viven en la misma escena; cambiar pantalla es solo modificar active del Layer, sin destruir ni reconstruir, el estado se conserva y las animaciones no se interrumpen. En un minijuego con pocas pantallas basta. En un juego grande, con muchos niveles y recursos pesados, sí conviene multi-escena y subpaquetes. Para minijuegos, escena única alcanza.

Plantilla de estructura de directorios

Ya sabes por qué usar escena única. Veamos cómo organizar los directorios.

Esta es la plantilla que fui afinando tras varios tropiezos (ejemplo con Cocos Creator 3.x):

assets/
├── Scenes/
│   └── Main.scene           # Única escena principal
├── Scripts/
│   ├── managers/
│   │   ├── GameManager.ts   # Estado y datos del juego
│   │   ├── UIManager.ts     # Lógica de cambio entre Layer
│   │   └── AudioManager.ts  # Gestión de audio
│   ├── layers/
│   │   ├── BootLayer.ts     # Lógica de pantalla de arranque
│   │   ├── MenuLayer.ts     # Lógica del menú principal
│   │   ├── GameLayer.ts     # Pantalla principal del juego
│   │   └── SettlementLayer.ts  # Pantalla de resultados
│   └── components/
│       ├── PlayerController.ts
│       └── EnemyAI.ts
├── Prefabs/
│   ├── UI/
│   │   ├── BootPanel.prefab    # Prefab UI de arranque
│   │   ├── MenuPanel.prefab    # UI del menú
│   │   ├── SettlementPanel.prefab
│   ├── Game/
│   │   ├── Player.prefab
│   │   ├── Obstacle.prefab
│   └── Effects/
│       └── Explosion.prefab
├── Resources/
│   ├── textures/             # Imágenes de carga dinámica
│   ├── audio/                # Archivos de audio
│   └── fonts/
└── bundles/                  # Subpaquetes Asset Bundle (WeChat)
    ├── core/                 # Paquete de recursos core
    └── levels/               # Paquete de recursos de niveles

Puntos clave:

Scenes solo tiene un archivo. Main.scene es la única escena principal, con Canvas, GameManager, cada Layer, etc. No metas más .scene ahí.

managers guarda scripts de gestión. GameManager se marca persistente con game.addPersistRootNode() y almacena puntuación, progreso, etc.; UIManager maneja el cambio entre Layer.

layers contiene la lógica de cada capa UI. Cada Layer tiene un Prefab (en Prefabs/UI), script adjunto, y se arrastra bajo Canvas en Main.scene.

bundles es para minijuegos WeChat/Douyin. Hay límite de tamaño del paquete principal; lo que pase de 4 MB va en Asset Bundle. Recursos core en core, niveles bajo demanda.

¿Tu proyecto está organizado así? Si no, conviene replantearlo.

Escena Boot: cómo diseñar la pantalla de arranque

Un dato: el motor Cocos Creator pesa unos 1.9 MB; con tus recursos, la primera carga suele tardar 3-5 segundos. Para un minijuego es mucho: el usuario abre el juego, mira pantalla negra o vacía 3 segundos y muchos cierran.

Boot Layer resuelve eso. Sus tareas son simples:

Mostrar progreso de carga. Que sepa que el juego está cargando, no colgado. Una barra o porcentaje basta; no te compliques: aún no has cargado todas las imágenes.

Precargar recursos core. Con resources.preload() o loadBundle() de Asset Bundle, carga antes imágenes y audio que necesitarás.

Mostrar la marca. Logo y una animación sencilla (por ejemplo zoom con fade-in) para reforzar la marca.

El flujo de arranque suele ser:

Inicialización del motor → Boot Layer visible → Precarga de recursos core → Carga completa → Cambio a MenuLayer

Ejemplo de código (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() {
        // Precargar recursos core
        this.preloadCoreAssets();
    }

    preloadCoreAssets() {
        resources.preloadDir('textures', (err, items) => {
            if (err) {
                console.error('Error en precarga:', err);
                return;
            }
            // Carga completa, cambiar al menú
            this.switchToMenu();
        });
    }

    updateProgress(current: number, total: number) {
        if (this.progressBar) {
            this.progressBar.progress = current / total;
        }
    }

    switchToMenu() {
        // Indicar a UIManager que cambie a MenuLayer
        // El código se muestra más adelante
    }
}

Optimización del paquete principal en WeChat: si publicas en WeChat, el límite del paquete principal es 4 MB. El resto va en Asset Bundle. Puedes cargar esos bundles en Boot Layer:

assetManager.loadBundle('levels', (err, bundle) => {
    if (err) return;
    console.log('Paquete de niveles cargado');
});

Pantalla de arranque H5 personalizada: Cocos Creator ofrece build-templates. Crea build-templates/web-mobile en la raíz del proyecto con un index.html propio y sustituyes la pantalla negra por defecto: animación de carga o imagen de marca.

Tutorial en el foro oficial: Creator | Pantalla de arranque personalizada para H5.

Con Boot Layer listo, viene el núcleo: cómo cambiar entre las cuatro capas.

Cuatro capas: del menú a los resultados

Esta es la parte central. La escena principal se ve así:

Main.scene
├── Canvas (contenedor UI)
│   ├── BootLayer      → Progreso de carga, marca
│   ├── MenuLayer      → Inicio, selección de nivel
│   ├── GameLayer      → Pantalla principal del juego
│   └── SettlementLayer → Resultados (puntuación, tiempo, continuar/reintentar)
├── GameManager (nodo persistente)
│   └─ Almacena: puntuación, progreso, ajustes del jugador
└── AudioRoot (nodo de audio)

Las cuatro Layer están bajo el mismo Canvas. Al cambiar, solo modificas active de una Layer; las demás siguen existiendo: no pierdes estado ni cortas animaciones.

La lógica de cambio vive en 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() {
        // Recoger todos los nodos 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; // Ocultar todas al inicio
            }
        });

        // Mostrar BootLayer
        this.switchLayer('BootLayer');
    }

    switchLayer(targetLayer: string) {
        // Ocultar capa actual
        const current = this.layers.get(this.currentLayer);
        if (current) {
            current.active = false;
        }

        // Mostrar capa destino
        const target = this.layers.get(targetLayer);
        if (target) {
            target.active = true;
            // Animación de fade-in (opcional)
            this.playFadeIn(target);
        }

        this.currentLayer = targetLayer;
    }

    playFadeIn(node: Node) {
        // Animación simple de escala con fade-in
        node.setScale(new Vec3(0.9, 0.9, 1));
        tween(node)
            .to(0.2, { scale: new Vec3(1, 1, 1) })
            .start();
    }
}

Clave: usa un Map para las referencias de Layer; así no buscas con getChildByName() en cada cambio.

Pasar datos a la pantalla de resultados: el jugador termina con 1200 puntos en 45 segundos. ¿Cómo llega eso a SettlementLayer?

Respuesta: GameManager.

GameManager es persistente y vive todo el juego. En GameLayer guardas puntuación y tiempo; al pasar a SettlementLayer, los lees desde GameManager.

// GameManager.ts (versión 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() {
        // Marcar como nodo persistente
        game.addPersistRootNode(this.node);
    }

    // GameLayer llama esto para guardar
    saveResult(score: number, time: number) {
        this.currentScore = score;
        this.playTime = time;
    }

    // SettlementLayer llama esto para leer
    getResult() {
        return {
            score: this.currentScore,
            time: this.playTime
        };
    }
}

Al mostrar SettlementLayer, lees GameManager:

// SettlementLayer.ts
onEnable() {
    const gameManager = find('GameManager')?.getComponent(GameManager);
    if (!gameManager) return;

    const result = gameManager.getResult();
    this.scoreLabel.string = `Puntuación: ${result.score}`;
    this.timeLabel.string = `Tiempo: ${result.time} s`;
}

Problema resuelto sin un sistema de eventos complejo: GameManager centraliza.

Nodo persistente: datos entre Layer

Ya lo mencionamos; aquí más detalle.

game.addPersistRootNode() es la API oficial de Cocos Creator para que un nodo no se destruya al cambiar de escena. En escena única no cambias de escena; ¿para qué entonces?

Sirve como centro de datos y eventos global.

Responsabilidades de GameManager:

Almacenar datos del jugador: puntuación, progreso, récord, niveles desbloqueados.
Almacenar ajustes: sonido, música, idioma.
Ofrecer eventos globales, por ejemplo “nivel completado”, que otras Layer pueden escuchar.

Precauciones:

El nodo persistente no va bajo Canvas. Canvas es UI y se manipula con el active de los Layer. El persistente va en la raíz de la escena.

Créalo a mano: nodo vacío “GameManager” en Main.scene, script GameManager, y en onLoad() llama game.addPersistRootNode(this.node).

Úsalo con moderación. Solo datos realmente compartidos entre pantallas; el estado UI de cada Layer (por ejemplo si un botón se muestra) déjalo en la Layer.

Ejemplo de estructura de datos:

// GameManager.ts (versión 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);
        // Leer datos del almacenamiento 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 y setters...
}

También incluye lectura/escritura con localStorage para persistir datos. Las plataformas de minijuegos lo soportan (WeChat usa wx.setStorageSync, pero localStorage encapsulado también funciona).

Resumen

Lista de decisiones de arquitectura para contrastar con tu proyecto:

Si haces un minijuego:

  • Arquitectura de escena única (un Main.scene)
  • Todas las capas UI bajo Canvas, cambio por Layer
  • BootLayer para progreso y precarga
  • GameManager persistente para datos
  • Directorios: assets/Scripts/managers/layers/Prefabs

Si haces un juego grande:

  • Multi-escena es necesaria (muchos niveles, recursos pesados)
  • La UI puede seguir la idea escena única + Layer
  • Asset Bundle para subpaquetes

Usé esta arquitectura en varios minijuegos, con pruebas y ajustes; creo que está bastante madura. Si tienes dudas, comenta o busca un proyecto de ejemplo en GitHub.

En el próximo artículo hablaré de animaciones al cambiar Layer: fade, deslizamiento, escala, para transiciones más fluidas.

Montar la arquitectura de escena única para minijuegos en Cocos Creator

Montar desde cero la arquitectura de un minijuego con Boot, escena principal y pantalla de resultados

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Crear la estructura de directorios del proyecto

    En assets, crea directorios como Scenes, Scripts/managers, Scripts/layers, Scripts/components, Prefabs/UI, Prefabs/Game, Resources, bundles, etc., organizando archivos por responsabilidad.
  2. 2

    Step 2: Crear Main.scene y GameManager

    Crea la única escena principal Main.scene, crea el nodo GameManager en la raíz y adjunta el script; en onLoad() llama a game.addPersistRootNode(this.node) para marcarlo como nodo persistente.
  3. 3

    Step 3: Crear cuatro prefabs de Layer

    Crea cuatro Prefab: BootLayer, MenuLayer, GameLayer y SettlementLayer; cada uno con su script lógico correspondiente, todos en Prefabs/UI.
  4. 4

    Step 4: Implementar la lógica de cambio en UIManager

    En UIManager.ts usa un Map para guardar referencias de todos los Layer, implementa switchLayer(targetLayer) cambiando la propiedad active, y añade animación de fade-in para mejorar la experiencia.
  5. 5

    Step 5: Implementar el flujo de carga de Boot Layer

    En BootLayer.ts llama a resources.preloadDir() para precargar recursos core, actualiza la barra de progreso y, al terminar, usa UIManager para cambiar a MenuLayer.

FAQ

¿Por qué se recomienda la arquitectura de escena única para minijuegos?
Evita el costo de destruir y reconstruir al cambiar de escena, la pérdida de estado y la interrupción de animaciones. Cambiar la propiedad active de los Layer mantiene el estado y hace las transiciones más fluidas.
¿Cómo se pasan los datos al cambiar de escena?
Usa GameManager como nodo persistente para almacenar datos entre pantallas. Puntuación, progreso de nivel, etc. viven en GameManager; la pantalla de resultados lee con gameManager.getResult().
¿En qué nivel va el nodo persistente?
Debe estar en la raíz de la escena, no bajo Canvas. Canvas es contenedor de UI y se manipula al cambiar Layer; el nodo persistente debe existir de forma independiente.
¿Qué hace Boot Layer?
Muestra el progreso de carga, precarga recursos core y presenta el logo de marca. Así el usuario sabe que el juego carga y no se ha quedado colgado.
¿Qué hacer si el paquete principal de WeChat supera 4 MB?
Usa Asset Bundle para cargar por subpaquetes. Recursos core en el bundle core, niveles en levels bundle; carga bajo demanda en Boot Layer y deja solo lo esencial en el paquete principal.

9 min de lectura · Publicado el: 19 may 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog